@on-par/factory-cli 0.9.2 → 0.9.3
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 -3
- package/dist/cli/doctor.d.ts +3 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +23 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +18 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +110 -28
- package/dist/cli/index.js.map +1 -1
- package/package.json +3 -3
package/dist/cli/index.js
CHANGED
|
@@ -2,14 +2,14 @@ import { fileURLToPath } from 'node:url';
|
|
|
2
2
|
// packages/cli/src/cli/index.ts — CLI entry point: factory <command> [options]
|
|
3
3
|
import { exec as execCb, execSync } from 'node:child_process';
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
5
|
+
import { appendFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
6
6
|
import { createRequire } from 'node:module';
|
|
7
7
|
import { arch, hostname as osHostname, release, platform, userInfo } from 'node:os';
|
|
8
8
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
9
9
|
import { promisify, styleText } from 'node:util';
|
|
10
10
|
import { BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION, BRANCH_PREFIX_OPTION_DESCRIPTION, INVALID_BRANCH_PREFIX_MESSAGE, resolveBranchPrefixOption, } from './branch-prefix.js';
|
|
11
|
-
import { acquirePortLease, aggregateCosts, appendKpiHistoryLine, applyRepoConfig, buildPhase, checkPhase, clearProxyState, closedWorkSkipReason, computeHealthKpis, ConstitutionLoader, createDefaultWorkSourceRegistry, createFileApprovalGate, createLogger, createLaneProxy, defaultFindPortListeners, describeEffectiveConfig, detectPostMergeDefects, diagnoseModels, drainSteering, estimateTrailingSpend, fetchDefectSources, fetchHumanEventSources, fetchSubscriptionUsage, formatKpiLines, formatUsageReport, getConstitutionsDir, getFactoryPaths, GITHUB_ISSUE_SOURCE, hasUnresolvedPark, InvalidArtifactsDirError, InvalidWorkRequestInputError, InvalidWorkspaceError, inspectPortLeases, isCommandAvailable, isProxyRunning, kpiBranchPrefixes, kpisToHistoryRecord, laneBaseUrl, laneEnv, LaneFileGuard, laneHostLabel, listQueuedSteering, loadFactoryConfigForRepo, loadModelsConfig, loadRepoConfig, loadRoutesConfig, LOCAL_BRIEF_SOURCE, localOnlyWorkspace, joinClassifierOutcomes, mergeClassifierOutcomes, parseClassifierOutcomes, summarizeClassifierOutcomes, formatClassifierReport, mergedPrRefs, ModelRegistry, ModelRouter, parkReasonFor, parseKpiHistory, parseQueue, partitionLocalQueueByActivity, phaseSnapshotFile, planPhase, provisionLaneContainer, ProviderBreaker, readEvents, readPortLeases, readQueue, readUsage, reapOrphanProcesses, reapStalePortLeases, reconstructKpiEvents, recordLeasePgid, releasePortLease, renderKpiReport, renderKpiTrend, resolveAutoFailover, resolveCodexDisabled, resolveDefectWindowDays, resolveBuildPublish, resolveSizeGateMode, parseSizeGateMode, parseMaxSlices, resolveSizeGateMaxSlices, SIZE_GATE_MODES, resolveDesignRegressionBlock, resolveReadinessUpdateIssueBody, resolveEffectiveModelPins, resolveEfficiencyPolicy, resolveLaneBreakerThreshold, resolveEnvironmentPorts, resolveEnvironmentProxy, resolveArtifactsDir, resolveIngestConfig, resolveLocalOnlyPolicy, resolveMergePolicy, resolvePlanApproval, resolveProcessGroupGraceMs, resolveSandboxPolicy, resolveSkipCI, resolveTimeouts, resolveUsageCap, resolveWatchdogPolicy, ReworkHistory, rewriteQueueForDecomposition, runAutoIngest, runIssue, shipPhase, summarizeEvent, formatCostTotal, touchLastEvent, touchRunActivity, validateQueue, watchUsage, worktreeWorkspace, writeBenchmarkArtifacts, writeLocalRunReport, writePhaseSnapshot, writeProxyState, } from '@on-par/factory-core';
|
|
12
|
-
import { acquirePidFile, createPlanReuseStore, checkBaseHealth, currentSlice, egressSummaryMessage, findSlicePlanComment, upsertSlicePlanComment, withSliceState, DEFAULT_GARDEN_MAX_CLUSTERS, DEFAULT_GARDEN_SINCE, capGardenClusters, clusterGarden, dedupGardenClusters, filterGardenOnlyNew, filterGardenWindow, markGardenTrackingUnknown, parseGardenDuration, readHarvestEvents, renderGardenJson, renderGardenReport, formatWorktreeLocation, laneWorktreePath, resolveWorktreeRoot, migrateRepoConfigToYaml, REPO_CONFIG_YAML_HEADER, branchFor, claudeConfigDirOverride, claudeKeychainService, cleanupWorktree, createDaemonLogSink, factoryBranchIssue, factoryBranchPrefixes, getIssueTitle, LEGACY_BRANCH_PREFIX, createDockerEngine, createFactorydServer, createRunRuntime, createShipExecutor, createGithubQueue, createLocalSmallDryRun, createOctokitFilingClient, createOctokitGreenPrClient, createOctokitQueueClient, daemonRuntimePaths, DEFAULT_FACTORYD_PORT, defaultRegistryPath, ensureDir, FACTORY_RUNTIME_CONFIG_KEYS, findUnmergedGreenPrs, formatGcReport, gitFetch, isAutoMergeBlocked, listMicroVms, listOrphanContainers, logCost, logEvent, microVmName, planQueueMigration, readCosts, readGithubQueueSnapshot, reapLaneWorktree, reapOrphanContainers, reapOrphanMicroVm, releaseRuntimeFiles, releaseStaleClaims, resolveBranchPrefix, resolveEffectiveConfig, worktreeSandboxFor, resolveExperimental, resolveFilingPolicy, resolveLocalOnly, resolvePrClassifierPolicy, resolveReviewFloorRules, RunLockHeldError, runOvernightQueue, findOpenPR, parseRemoteSlug, setupWorktree, shellEscape, shouldProxyEgress, slugify, startEgressProxy, sweepWorktrees, watchChecks, watchChecksWithInfraRerun, withAdmissionGuard, withFileLock, withGitLock, withRunLock, wrapCommandInSandbox, writePortFile, } from '@on-par/factory-core/internal';
|
|
11
|
+
import { acquirePortLease, aggregateCosts, appendKpiHistoryLine, applyRepoConfig, buildPhase, checkPhase, clearProxyState, closedWorkSkipReason, computeHealthKpis, ConstitutionLoader, createDefaultWorkSourceRegistry, createFileApprovalGate, createLogger, createLaneProxy, defaultFindPortListeners, describeEffectiveConfig, detectPostMergeDefects, diagnoseModels, probeDeclaredHarnesses, drainSteering, estimateTrailingSpend, fetchDefectSources, fetchHumanEventSources, fetchSubscriptionUsage, formatKpiLines, formatUsageReport, getConstitutionsDir, getFactoryPaths, GITHUB_ISSUE_SOURCE, hasUnresolvedPark, InvalidArtifactsDirError, InvalidWorkRequestInputError, InvalidWorkspaceError, inspectPortLeases, isCommandAvailable, isProxyRunning, kpiBranchPrefixes, kpisToHistoryRecord, laneBaseUrl, laneEnv, LaneFileGuard, laneHostLabel, listQueuedSteering, loadFactoryConfigForRepo, loadModelsConfig, loadRepoConfig, loadRoutesConfig, LOCAL_BRIEF_SOURCE, localOnlyWorkspace, joinClassifierOutcomes, mergeClassifierOutcomes, parseClassifierOutcomes, summarizeClassifierOutcomes, formatClassifierReport, mergedPrRefs, ModelRegistry, ModelRouter, parkReasonFor, parseKpiHistory, parseQueue, partitionLocalQueueByActivity, phaseSnapshotFile, planPhase, provisionLaneContainer, ProviderBreaker, readEvents, readPortLeases, readQueue, readUsage, reapOrphanProcesses, reapStalePortLeases, reconstructKpiEvents, recordLeasePgid, releasePortLease, renderKpiReport, renderKpiTrend, resolveAutoFailover, resolveCodexDisabled, resolveDefectWindowDays, resolveBuildPublish, resolveSizeGateMode, parseSizeGateMode, parseMaxSlices, resolveSizeGateMaxSlices, SIZE_GATE_MODES, resolveDesignRegressionBlock, resolveReadinessUpdateIssueBody, resolveEffectiveModelPins, resolveEfficiencyPolicy, resolveLaneBreakerThreshold, resolveEnvironmentPorts, resolveEnvironmentProxy, resolveArtifactsDir, resolveIngestConfig, resolveLocalOnlyPolicy, resolveMergePolicy, resolvePlanApproval, resolveProcessGroupGraceMs, resolveSandboxPolicy, resolveSkipCI, resolveTimeouts, resolveUsageCap, resolveWatchdogPolicy, ReworkHistory, rewriteQueueForDecomposition, runAutoIngest, runIssue, shipPhase, summarizeEvent, formatCostTotal, touchLastEvent, touchRunActivity, validateQueue, watchUsage, worktreeWorkspace, writeBenchmarkArtifacts, writeLocalRunReport, writePhaseSnapshot, writeProxyState, } from '@on-par/factory-core';
|
|
12
|
+
import { acquirePidFile, createPlanReuseStore, checkBaseHealth, currentSlice, egressSummaryMessage, findSlicePlanComment, upsertSlicePlanComment, withSliceState, DEFAULT_GARDEN_MAX_CLUSTERS, DEFAULT_GARDEN_SINCE, capGardenClusters, clusterGarden, dedupGardenClusters, filterGardenOnlyNew, filterGardenWindow, markGardenTrackingUnknown, parseGardenDuration, readHarvestEvents, renderGardenJson, renderGardenReport, formatWorktreeLocation, laneWorktreePath, resolveWorktreeRoot, migrateRepoConfigToYaml, REPO_CONFIG_YAML_HEADER, branchFor, claudeConfigDirOverride, claudeKeychainService, cleanupWorktree, createDaemonLogSink, factoryBranchIssue, factoryBranchPrefixes, getIssueTitle, LEGACY_BRANCH_PREFIX, createDockerEngine, createFactorydServer, createRunRuntime, createShipExecutor, createGithubQueue, createLocalSmallDryRun, createOctokitFilingClient, createOctokitGreenPrClient, createOctokitQueueClient, daemonRuntimePaths, DEFAULT_FACTORYD_PORT, defaultRegistryPath, ensureDir, FACTORY_RUNTIME_CONFIG_KEYS, findUnmergedGreenPrs, formatGcReport, gitFetch, isAutoMergeBlocked, listMicroVms, listOrphanContainers, logCost, logEvent, microVmName, planQueueMigration, readCosts, readGithubQueueSnapshot, reapLaneWorktree, reapOrphanContainers, reapOrphanMicroVm, releaseRuntimeFiles, releaseStaleClaims, resolveBranchPrefix, resolveEffectiveConfig, worktreeSandboxFor, resolveExperimental, resolveFilingPolicy, resolveLocalOnly, resolvePrClassifierPolicy, resolveReviewFloorRules, RunLockHeldError, runOvernightQueue, findOpenPR, parseRemoteSlug, setupWorktree, shellEscape, listBuiltInCheckers, shouldProxyEgress, slugify, startEgressProxy, sweepWorktrees, watchChecks, watchChecksWithInfraRerun, withAdmissionGuard, withFileLock, withGitLock, withRunLock, wrapCommandInSandbox, writePortFile, } from '@on-par/factory-core/internal';
|
|
13
13
|
import { runTui } from '@on-par/factory-tui';
|
|
14
14
|
import { Command } from 'commander';
|
|
15
15
|
import { admissionStateReaderFor, runQueueReconcile } from './admission.js';
|
|
@@ -29,7 +29,7 @@ import { buildUsageJson } from './usage-json.js';
|
|
|
29
29
|
import { parseResetIssues, runReset } from './reset.js';
|
|
30
30
|
import { cmdDaemonLogs, cmdDaemonStart, cmdDaemonStatus, cmdDaemonStop, createAutoUpdateDeps, DaemonCtlError, detectInstall, loadDaemonFileConfig, recoverStaleDrain, resolveFactorydConfig, runAutoUpdateLoop, } from './daemon.js';
|
|
31
31
|
import { checkForUpdate, cmdSelfUpdateCheck } from './self-update.js';
|
|
32
|
-
import { analyzeEventLog, claudeKeychainCheck, doctorFailed, eventLogCheck, formatClaimReconcileReport, formatContainerReconcileReport, formatDoctorChecks, formatMicroVmReconcileReport, formatReconcileReport, formatWorktreeReconcileReport, hasSandboxRuntime, leaseChecks, keychainPreflightError, orphanContainerChecks, orphanMicroVmChecks, runDoctorChecks, sandboxAuthSkipDetail, sandboxClaudeAuthChecks, unmergedGreenPrChecks, } from './doctor.js';
|
|
32
|
+
import { analyzeEventLog, claudeKeychainCheck, doctorFailed, eventLogCheck, formatClaimReconcileReport, formatContainerReconcileReport, formatDoctorChecks, formatMicroVmReconcileReport, formatReconcileReport, formatWorktreeReconcileReport, declaredHarnessChecks, hasSandboxRuntime, leaseChecks, keychainPreflightError, orphanContainerChecks, orphanMicroVmChecks, runDoctorChecks, sandboxAuthSkipDetail, sandboxClaudeAuthChecks, unmergedGreenPrChecks, } from './doctor.js';
|
|
33
33
|
import { DOCS_URL, formatOverview, missingClaudeCliMessage, missingTokenMessage, notInitializedMessage, } from './first-run.js';
|
|
34
34
|
import { cmdHostedSmoke } from './hosted.js';
|
|
35
35
|
import { cmdHostedQueue } from './hosted-queue.js';
|
|
@@ -144,6 +144,7 @@ const DEFAULT_CORE_OPS = {
|
|
|
144
144
|
watchUsage,
|
|
145
145
|
fetchSubscriptionUsage,
|
|
146
146
|
diagnoseModels,
|
|
147
|
+
probeDeclaredHarnesses,
|
|
147
148
|
writeLocalRunReport,
|
|
148
149
|
};
|
|
149
150
|
/** factory-core classes/ops for the current main() invocation; DEFAULT_CORE_OPS outside it. */
|
|
@@ -162,6 +163,7 @@ function resolveCoreOps(overrides, base = coreOps) {
|
|
|
162
163
|
watchUsage: overrides.watchUsage ?? base.watchUsage,
|
|
163
164
|
fetchSubscriptionUsage: overrides.fetchSubscriptionUsage ?? base.fetchSubscriptionUsage,
|
|
164
165
|
diagnoseModels: overrides.diagnoseModels ?? base.diagnoseModels,
|
|
166
|
+
probeDeclaredHarnesses: overrides.probeDeclaredHarnesses ?? base.probeDeclaredHarnesses,
|
|
165
167
|
writeLocalRunReport: overrides.writeLocalRunReport ?? base.writeLocalRunReport,
|
|
166
168
|
};
|
|
167
169
|
}
|
|
@@ -297,8 +299,14 @@ function writeIfAbsent(path, content, label, force = false) {
|
|
|
297
299
|
}
|
|
298
300
|
/** One-line onboarding reachability summary, e.g. "policy=auto, 3/7 models reachable". */
|
|
299
301
|
export function formatInitReachability(diagnoses) {
|
|
302
|
+
return `policy=auto, ${formatReachabilityCount(diagnoses)}`;
|
|
303
|
+
}
|
|
304
|
+
/** Shared "K/N models reachable[, U unverified (no auth evidence)]" count for init and models --doctor. */
|
|
305
|
+
export function formatReachabilityCount(diagnoses) {
|
|
300
306
|
const reachable = diagnoses.filter((d) => d.reachable).length;
|
|
301
|
-
|
|
307
|
+
const unverified = diagnoses.filter((d) => !d.reachable && d.unverified).length;
|
|
308
|
+
const base = `${reachable}/${diagnoses.length} models reachable`;
|
|
309
|
+
return unverified > 0 ? `${base}, ${unverified} unverified (no auth evidence)` : base;
|
|
302
310
|
}
|
|
303
311
|
async function cmdInit(opts = {}) {
|
|
304
312
|
const repoRoot = await getRepoRoot();
|
|
@@ -321,15 +329,17 @@ async function cmdInit(opts = {}) {
|
|
|
321
329
|
const constitutionPath = resolve(paths.root, 'constitution.md');
|
|
322
330
|
const gitignorePath = resolve(paths.root, '.gitignore');
|
|
323
331
|
writeIfAbsent(configPath, buildInitConfig(configPath.endsWith('.json') ? 'json' : 'yaml'), `.factory/${basename(configPath)}`, force);
|
|
324
|
-
// Constitution scaffold:
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
|
|
332
|
+
// Constitution scaffold: the active product's bundled file, else the default-code constitution.
|
|
333
|
+
const constitutionsDir = configLoaders.getConstitutionsDir();
|
|
334
|
+
const active = readActiveProduct(paths.product);
|
|
335
|
+
const activeFile = active ? resolve(constitutionsDir, `${active}.md`) : undefined;
|
|
336
|
+
const source = activeFile && existsSync(activeFile) ? activeFile : resolve(constitutionsDir, 'default-code.md');
|
|
337
|
+
writeIfAbsent(constitutionPath, readFileSync(source, 'utf-8'), '.factory/constitution.md', force);
|
|
328
338
|
writeIfAbsent(gitignorePath, 'state/\n', '.factory/.gitignore', force);
|
|
329
339
|
// Doctor-style validation (policy=auto, N models reachable) — informational, never fails init.
|
|
330
340
|
const modelsConfig = applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot));
|
|
331
341
|
const registry = new coreOps.ModelRegistry(modelsConfig);
|
|
332
|
-
const diagnoses = coreOps.diagnoseModels(registry,
|
|
342
|
+
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), resolveExperimental(), resolveLocalOnly());
|
|
333
343
|
console.log(formatInitReachability(diagnoses));
|
|
334
344
|
if (!hasReachableWorker(diagnoses)) {
|
|
335
345
|
console.log(styleText('yellow', 'No worker model reachable yet — see `factory doctor` and `factory models --doctor`.'));
|
|
@@ -604,6 +614,40 @@ export function scaffoldConstitution(template, product) {
|
|
|
604
614
|
.join(' ');
|
|
605
615
|
return skeleton.replaceAll('<product-name>', JSON.stringify(product)).replaceAll('<Product>', display);
|
|
606
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* True when `existing` is an untouched scaffold: byte-identical (CRLF and trailing whitespace
|
|
619
|
+
* normalised) to a bundled constitution or to the legacy `_template.md` scaffold for `repoName`.
|
|
620
|
+
* Exact equality only — an edited file is never treated as a scaffold.
|
|
621
|
+
*/
|
|
622
|
+
export function isUnmodifiedConstitutionScaffold(existing, constitutionsDir, repoName) {
|
|
623
|
+
const norm = (s) => s.replace(/\r\n/g, '\n').trimEnd();
|
|
624
|
+
const target = norm(existing);
|
|
625
|
+
if (target === '')
|
|
626
|
+
return true;
|
|
627
|
+
let names = [];
|
|
628
|
+
try {
|
|
629
|
+
names = readdirSync(constitutionsDir).filter((f) => f.endsWith('.md') && !f.startsWith('_'));
|
|
630
|
+
}
|
|
631
|
+
catch {
|
|
632
|
+
// unreadable dir: no bundled files to compare against
|
|
633
|
+
}
|
|
634
|
+
for (const f of names) {
|
|
635
|
+
try {
|
|
636
|
+
if (norm(readFileSync(resolve(constitutionsDir, f), 'utf-8')) === target)
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
// skip unreadable entries
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
try {
|
|
644
|
+
const template = readFileSync(resolve(constitutionsDir, '_template.md'), 'utf-8');
|
|
645
|
+
return norm(scaffoldConstitution(template, repoName)) === target;
|
|
646
|
+
}
|
|
647
|
+
catch {
|
|
648
|
+
return false;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
607
651
|
export async function cmdConstitution(opts, deps = {}) {
|
|
608
652
|
const loader = new (resolveCoreOps(deps).ConstitutionLoader)();
|
|
609
653
|
if (typeof opts.init === 'string' || opts.init === true) {
|
|
@@ -634,8 +678,21 @@ export async function cmdConstitution(opts, deps = {}) {
|
|
|
634
678
|
}
|
|
635
679
|
if (opts.list) {
|
|
636
680
|
const products = loader.listProducts();
|
|
637
|
-
for (const p of products)
|
|
638
|
-
|
|
681
|
+
for (const p of products) {
|
|
682
|
+
try {
|
|
683
|
+
const checkers = loader.load(p).checkers.join(', ');
|
|
684
|
+
console.log(` ${p} — ${checkers || '(no checkers)'}`);
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
console.log(` ${p}`);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (opts.checkers) {
|
|
693
|
+
for (const c of listBuiltInCheckers())
|
|
694
|
+
console.log(` ${c.name} — ${c.description}`);
|
|
695
|
+
console.log(" custom_<name> — agent-judged checker graded against the constitution body (declare it in the constitution's checkers list)");
|
|
639
696
|
return;
|
|
640
697
|
}
|
|
641
698
|
if (opts.product) {
|
|
@@ -647,25 +704,33 @@ export async function cmdConstitution(opts, deps = {}) {
|
|
|
647
704
|
const paths = getFactoryPaths(repoRoot);
|
|
648
705
|
ensureDir(paths.root);
|
|
649
706
|
const constitutionPath = resolve(paths.root, 'constitution.md');
|
|
650
|
-
|
|
707
|
+
const constitutionsDir = configLoaders.getConstitutionsDir();
|
|
708
|
+
const scaffold = existsSync(constitutionPath) &&
|
|
709
|
+
isUnmodifiedConstitutionScaffold(readFileSync(constitutionPath, 'utf-8'), constitutionsDir, basename(repoRoot));
|
|
710
|
+
if (existsSync(constitutionPath) && !opts.force && !scaffold) {
|
|
651
711
|
throw new CliExitError('.factory/constitution.md already exists — use --force to overwrite', 1);
|
|
652
712
|
}
|
|
653
|
-
const content = readFileSync(resolve(
|
|
654
|
-
writeIfAbsent(constitutionPath, content, '.factory/constitution.md', opts.force);
|
|
713
|
+
const content = readFileSync(resolve(constitutionsDir, `${opts.product}.md`), 'utf-8');
|
|
714
|
+
writeIfAbsent(constitutionPath, content, '.factory/constitution.md', opts.force || scaffold);
|
|
655
715
|
ensureDir(paths.state);
|
|
656
716
|
writeFileSync(paths.product, opts.product);
|
|
657
717
|
console.log(styleText('green', `Active product: ${opts.product}`));
|
|
658
718
|
return;
|
|
659
719
|
}
|
|
660
|
-
throw new CliExitError('usage: factory constitution --init <product> | --list | --product <name>', 2);
|
|
720
|
+
throw new CliExitError('usage: factory constitution --init <product> | --list | --checkers | --product <name>', 2);
|
|
721
|
+
}
|
|
722
|
+
/** Single owner of the per-model status icon, shared by `factory models` and `models --doctor` (#2402). */
|
|
723
|
+
export function modelStatusIcon(d) {
|
|
724
|
+
return d.reachable ? styleText('green', '✅') : d.unverified ? styleText('yellow', '⚠️') : styleText('red', '❌');
|
|
661
725
|
}
|
|
662
726
|
export function formatDoctorReport(diagnoses) {
|
|
663
727
|
const lines = [styleText('bold', '\n== Model Doctor ==')];
|
|
664
728
|
for (const d of diagnoses) {
|
|
665
|
-
const icon = d
|
|
729
|
+
const icon = modelStatusIcon(d);
|
|
666
730
|
const tiers = d.tiers.join('/');
|
|
667
731
|
lines.push(` ${icon} ${d.model} provider=${d.provider} tier=${tiers} — ${d.reason}`);
|
|
668
732
|
}
|
|
733
|
+
lines.push(` ${formatReachabilityCount(diagnoses)}`);
|
|
669
734
|
return lines.join('\n');
|
|
670
735
|
}
|
|
671
736
|
export function hasReachableWorker(diagnoses) {
|
|
@@ -684,17 +749,23 @@ function ollamaModelSet() {
|
|
|
684
749
|
return undefined;
|
|
685
750
|
}
|
|
686
751
|
}
|
|
752
|
+
/** Probes shared by every doctor-style reachability check (init, models --doctor, overnight
|
|
753
|
+
* preflight), so their counts agree. */
|
|
754
|
+
export function modelDoctorProbes() {
|
|
755
|
+
const ollamaModels = ollamaModelSet();
|
|
756
|
+
return {
|
|
757
|
+
commandAvailable: coreOps.isCommandAvailable,
|
|
758
|
+
ollamaModelPresent: ollamaModels ? (model) => ollamaModels.has(model) : undefined,
|
|
759
|
+
};
|
|
760
|
+
}
|
|
687
761
|
async function cmdModels(opts = {}) {
|
|
688
762
|
const repoRoot = await getRepoRoot();
|
|
689
763
|
const modelsConfig = applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot));
|
|
690
764
|
const registry = new coreOps.ModelRegistry(modelsConfig);
|
|
691
765
|
const allowExperimental = resolveExperimental();
|
|
692
766
|
const localOnly = resolveLocalOnly();
|
|
767
|
+
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), allowExperimental, localOnly);
|
|
693
768
|
if (opts.doctor) {
|
|
694
|
-
const ollamaModels = ollamaModelSet();
|
|
695
|
-
const diagnoses = coreOps.diagnoseModels(registry, {
|
|
696
|
-
ollamaModelPresent: ollamaModels ? (model) => ollamaModels.has(model) : undefined,
|
|
697
|
-
}, allowExperimental, localOnly);
|
|
698
769
|
console.log(formatDoctorReport(diagnoses));
|
|
699
770
|
if (!hasReachableWorker(diagnoses)) {
|
|
700
771
|
throw new CliExitError('factory: no worker model is reachable — fix the reasons above before running a queue', 1);
|
|
@@ -702,12 +773,13 @@ async function cmdModels(opts = {}) {
|
|
|
702
773
|
return;
|
|
703
774
|
}
|
|
704
775
|
console.log(styleText('bold', '\n== Available Models =='));
|
|
776
|
+
const byModel = new Map(diagnoses.map((d) => [d.model, d]));
|
|
705
777
|
for (const m of registry.list()) {
|
|
706
778
|
const tiers = registry.getTiers(m).join('/');
|
|
707
779
|
const est = registry.estimateCost(m, 1_000_000, 1_000_000);
|
|
708
780
|
const cost = est === null ? 'unpriced' : `$${est.toFixed(2)}/M`;
|
|
709
|
-
const
|
|
710
|
-
const avail =
|
|
781
|
+
const d = byModel.get(m);
|
|
782
|
+
const avail = d ? modelStatusIcon(d) : styleText('red', '❌');
|
|
711
783
|
const tag = registry.isExperimental(m) ? styleText('yellow', ' [experimental]') : '';
|
|
712
784
|
console.log(` ${avail} ${m} tier=${tiers} ${cost}${tag}`);
|
|
713
785
|
}
|
|
@@ -1730,7 +1802,8 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1730
1802
|
resolveConstitution: () =>
|
|
1731
1803
|
// Resolved once here — runIssue calls this exactly once and reuses the value for
|
|
1732
1804
|
// every phase, so the build worker can never author the standards it is graded by.
|
|
1733
|
-
|
|
1805
|
+
// A local-only workspace is caller-provided, not a checkout of repoRoot, so it keeps its own constitution.
|
|
1806
|
+
constitutionLoader.resolve(worktree, product, factoryConfig.paths?.constitution, ctx?.localOnly ? undefined : repoRoot),
|
|
1734
1807
|
createApprovalGate: () => createFileApprovalGate({ dir: paths.approvals, timeoutMs: timeouts.approval * 1000 }),
|
|
1735
1808
|
drainSteering: () => drainSteering(paths.steering, issueNum, worktree),
|
|
1736
1809
|
reworkHistory,
|
|
@@ -2220,8 +2293,7 @@ async function cmdLocalSmallOvernight(opts) {
|
|
|
2220
2293
|
if (!coreOps.isCommandAvailable('claude'))
|
|
2221
2294
|
return { ok: false, reason: missingClaudeCliMessage() };
|
|
2222
2295
|
const registry = new coreOps.ModelRegistry(applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot)));
|
|
2223
|
-
const
|
|
2224
|
-
const diagnoses = coreOps.diagnoseModels(registry, { ollamaModelPresent: ollamaModels ? (m) => ollamaModels.has(m) : undefined }, resolveExperimental(), true);
|
|
2296
|
+
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), resolveExperimental(), true);
|
|
2225
2297
|
if (!hasReachableWorker(diagnoses)) {
|
|
2226
2298
|
const reasons = diagnoses.filter((d) => !d.reachable).map((d) => `${d.model}: ${d.reason}`);
|
|
2227
2299
|
return { ok: false, reason: `no reachable local worker model — ${reasons.join('; ')}` };
|
|
@@ -4383,6 +4455,15 @@ async function cmdDoctor(opts = {}) {
|
|
|
4383
4455
|
checks.push(stopSentinelCheck(readStopFileStatus(paths)));
|
|
4384
4456
|
const eventsContent = existsSync(paths.events) ? readFileSync(paths.events, 'utf-8') : null;
|
|
4385
4457
|
checks.push(eventLogCheck(eventsContent === null ? null : analyzeEventLog(eventsContent)));
|
|
4458
|
+
let harnessProbes = [];
|
|
4459
|
+
try {
|
|
4460
|
+
const registry = new coreOps.ModelRegistry(applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot)));
|
|
4461
|
+
harnessProbes = coreOps.probeDeclaredHarnesses(registry);
|
|
4462
|
+
}
|
|
4463
|
+
catch {
|
|
4464
|
+
// models config problems are reported by `factory models --doctor`; doctor keeps going
|
|
4465
|
+
}
|
|
4466
|
+
checks.push(...declaredHarnessChecks(harnessProbes));
|
|
4386
4467
|
// Full run-lifecycle reap (#998): dead-run worktrees and their branches. Uses the same
|
|
4387
4468
|
// GitHub evidence and the same lock pairing as `factory worktree gc` (cmdWorktreeGc) —
|
|
4388
4469
|
// a merged or closed PR is what makes a worktree reapable.
|
|
@@ -4555,8 +4636,9 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4555
4636
|
.command('constitution')
|
|
4556
4637
|
.description('Manage product constitutions — .factory/constitution.md in this repo is the source of truth')
|
|
4557
4638
|
.option('--init [product]', 'Write .factory/constitution.md in this repo, scaffolded from the template (an optional name overrides the display name)')
|
|
4558
|
-
.option('--list', 'List available constitutions')
|
|
4559
|
-
.option('--
|
|
4639
|
+
.option('--list', 'List available constitutions with their checkers')
|
|
4640
|
+
.option('--checkers', 'List every built-in checker name with a one-line description')
|
|
4641
|
+
.option('--product <name>', 'Seed .factory/constitution.md from the bundled constitution for <name>; replaces an unmodified scaffold without --force')
|
|
4560
4642
|
.option('--force', 'With --init or --product, overwrite an existing .factory/constitution.md')
|
|
4561
4643
|
.action((opts) => cmdConstitution(opts));
|
|
4562
4644
|
program
|