@on-par/factory-cli 0.9.1 → 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/daemon.d.ts +60 -2
- package/dist/cli/daemon.d.ts.map +1 -1
- package/dist/cli/daemon.js +249 -2
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/doctor.d.ts +14 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +78 -6
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/feedback.d.ts +18 -2
- package/dist/cli/feedback.d.ts.map +1 -1
- package/dist/cli/feedback.js +38 -4
- package/dist/cli/feedback.js.map +1 -1
- package/dist/cli/index.d.ts +23 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +190 -44
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/update-state.d.ts +2 -0
- package/dist/cli/update-state.d.ts.map +1 -1
- package/dist/cli/update-state.js +5 -1
- package/dist/cli/update-state.js.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/entry.d.ts +1 -1
- package/dist/entry.d.ts.map +1 -1
- package/dist/entry.js +9 -2
- package/dist/entry.js.map +1 -1
- package/dist/node-version.d.ts +10 -0
- package/dist/node-version.d.ts.map +1 -0
- package/dist/node-version.js +15 -0
- package/dist/node-version.js.map +1 -0
- 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, 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, 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';
|
|
@@ -27,15 +27,15 @@ import { buildStatusUpdateJson, formatUpdateStartupLine, formatUpdateStatusLine,
|
|
|
27
27
|
import { buildStopResumeJson } from './stop-resume-json.js';
|
|
28
28
|
import { buildUsageJson } from './usage-json.js';
|
|
29
29
|
import { parseResetIssues, runReset } from './reset.js';
|
|
30
|
-
import { cmdDaemonLogs, cmdDaemonStart, cmdDaemonStatus, cmdDaemonStop, DaemonCtlError, detectInstall, loadDaemonFileConfig, resolveFactorydConfig, } from './daemon.js';
|
|
31
|
-
import { cmdSelfUpdateCheck } from './self-update.js';
|
|
32
|
-
import { analyzeEventLog, claudeKeychainCheck, doctorFailed, eventLogCheck, formatClaimReconcileReport, formatContainerReconcileReport, formatDoctorChecks, formatMicroVmReconcileReport, formatReconcileReport, formatWorktreeReconcileReport, leaseChecks, keychainPreflightError, orphanContainerChecks, orphanMicroVmChecks, runDoctorChecks, sandboxClaudeAuthChecks, unmergedGreenPrChecks, } from './doctor.js';
|
|
30
|
+
import { cmdDaemonLogs, cmdDaemonStart, cmdDaemonStatus, cmdDaemonStop, createAutoUpdateDeps, DaemonCtlError, detectInstall, loadDaemonFileConfig, recoverStaleDrain, resolveFactorydConfig, runAutoUpdateLoop, } from './daemon.js';
|
|
31
|
+
import { checkForUpdate, cmdSelfUpdateCheck } from './self-update.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';
|
|
36
36
|
import { cmdHostedRunner } from './hosted-runner.js';
|
|
37
37
|
import { resolveFactoryCheckoutCommit, runFilingPreview } from './filing.js';
|
|
38
|
-
import { buildFeedbackDeps, runFeedback } from './feedback.js';
|
|
38
|
+
import { buildFeedbackDeps, resolveFeedbackRepo, runFeedback } from './feedback.js';
|
|
39
39
|
import { ensureFactoryExcluded, writeSampleQueue } from './init-files.js';
|
|
40
40
|
import { cmdLogs } from './logs.js';
|
|
41
41
|
import { applyHelpGroups } from './help-groups.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`.'));
|
|
@@ -491,10 +501,34 @@ export async function runFeedbackCommand(prUrl, opts, deps, log = console.log) {
|
|
|
491
501
|
throw err;
|
|
492
502
|
}
|
|
493
503
|
}
|
|
504
|
+
/** owner/name of the checkout's origin remote, or null when there is none or it is not a GitHub remote. Never throws. */
|
|
505
|
+
export async function readOriginSlug(shell = shellOps) {
|
|
506
|
+
try {
|
|
507
|
+
const { stdout } = await shell.exec('git remote get-url origin');
|
|
508
|
+
return parseRemoteSlug(stdout.trim());
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
494
514
|
export async function cmdFeedback(prUrl, opts) {
|
|
495
515
|
const paths = getFactoryPaths(await getRepoRoot());
|
|
496
516
|
const prefix = opts.branchPrefix === undefined ? resolveBranchPrefix() : branchPrefixOrExit(opts.branchPrefix);
|
|
497
|
-
|
|
517
|
+
let repo;
|
|
518
|
+
try {
|
|
519
|
+
repo = resolveFeedbackRepo({
|
|
520
|
+
flag: opts.repo,
|
|
521
|
+
config: configLoaders.loadFactoryConfigForRepo(paths.config).filing.repo,
|
|
522
|
+
remote: await readOriginSlug(),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
throw new CliExitError(err instanceof Error ? err.message : String(err), 2);
|
|
527
|
+
}
|
|
528
|
+
await runFeedbackCommand(prUrl, opts, buildFeedbackDeps(getOctokit(), paths, prefix, createOctokitFilingClient, {
|
|
529
|
+
repo,
|
|
530
|
+
shareLocal: opts.includeLocalPaths === true,
|
|
531
|
+
}));
|
|
498
532
|
}
|
|
499
533
|
async function resolveFactoryCommit() {
|
|
500
534
|
return resolveFactoryCheckoutCommit(dirname(fileURLToPath(import.meta.url)), async (cmd, cwd) => (await shellOps.exec(cmd, { cwd })).stdout);
|
|
@@ -580,6 +614,40 @@ export function scaffoldConstitution(template, product) {
|
|
|
580
614
|
.join(' ');
|
|
581
615
|
return skeleton.replaceAll('<product-name>', JSON.stringify(product)).replaceAll('<Product>', display);
|
|
582
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
|
+
}
|
|
583
651
|
export async function cmdConstitution(opts, deps = {}) {
|
|
584
652
|
const loader = new (resolveCoreOps(deps).ConstitutionLoader)();
|
|
585
653
|
if (typeof opts.init === 'string' || opts.init === true) {
|
|
@@ -610,8 +678,21 @@ export async function cmdConstitution(opts, deps = {}) {
|
|
|
610
678
|
}
|
|
611
679
|
if (opts.list) {
|
|
612
680
|
const products = loader.listProducts();
|
|
613
|
-
for (const p of products)
|
|
614
|
-
|
|
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)");
|
|
615
696
|
return;
|
|
616
697
|
}
|
|
617
698
|
if (opts.product) {
|
|
@@ -623,25 +704,33 @@ export async function cmdConstitution(opts, deps = {}) {
|
|
|
623
704
|
const paths = getFactoryPaths(repoRoot);
|
|
624
705
|
ensureDir(paths.root);
|
|
625
706
|
const constitutionPath = resolve(paths.root, 'constitution.md');
|
|
626
|
-
|
|
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) {
|
|
627
711
|
throw new CliExitError('.factory/constitution.md already exists — use --force to overwrite', 1);
|
|
628
712
|
}
|
|
629
|
-
const content = readFileSync(resolve(
|
|
630
|
-
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);
|
|
631
715
|
ensureDir(paths.state);
|
|
632
716
|
writeFileSync(paths.product, opts.product);
|
|
633
717
|
console.log(styleText('green', `Active product: ${opts.product}`));
|
|
634
718
|
return;
|
|
635
719
|
}
|
|
636
|
-
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', '❌');
|
|
637
725
|
}
|
|
638
726
|
export function formatDoctorReport(diagnoses) {
|
|
639
727
|
const lines = [styleText('bold', '\n== Model Doctor ==')];
|
|
640
728
|
for (const d of diagnoses) {
|
|
641
|
-
const icon = d
|
|
729
|
+
const icon = modelStatusIcon(d);
|
|
642
730
|
const tiers = d.tiers.join('/');
|
|
643
731
|
lines.push(` ${icon} ${d.model} provider=${d.provider} tier=${tiers} — ${d.reason}`);
|
|
644
732
|
}
|
|
733
|
+
lines.push(` ${formatReachabilityCount(diagnoses)}`);
|
|
645
734
|
return lines.join('\n');
|
|
646
735
|
}
|
|
647
736
|
export function hasReachableWorker(diagnoses) {
|
|
@@ -660,17 +749,23 @@ function ollamaModelSet() {
|
|
|
660
749
|
return undefined;
|
|
661
750
|
}
|
|
662
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
|
+
}
|
|
663
761
|
async function cmdModels(opts = {}) {
|
|
664
762
|
const repoRoot = await getRepoRoot();
|
|
665
763
|
const modelsConfig = applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot));
|
|
666
764
|
const registry = new coreOps.ModelRegistry(modelsConfig);
|
|
667
765
|
const allowExperimental = resolveExperimental();
|
|
668
766
|
const localOnly = resolveLocalOnly();
|
|
767
|
+
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), allowExperimental, localOnly);
|
|
669
768
|
if (opts.doctor) {
|
|
670
|
-
const ollamaModels = ollamaModelSet();
|
|
671
|
-
const diagnoses = coreOps.diagnoseModels(registry, {
|
|
672
|
-
ollamaModelPresent: ollamaModels ? (model) => ollamaModels.has(model) : undefined,
|
|
673
|
-
}, allowExperimental, localOnly);
|
|
674
769
|
console.log(formatDoctorReport(diagnoses));
|
|
675
770
|
if (!hasReachableWorker(diagnoses)) {
|
|
676
771
|
throw new CliExitError('factory: no worker model is reachable — fix the reasons above before running a queue', 1);
|
|
@@ -678,12 +773,13 @@ async function cmdModels(opts = {}) {
|
|
|
678
773
|
return;
|
|
679
774
|
}
|
|
680
775
|
console.log(styleText('bold', '\n== Available Models =='));
|
|
776
|
+
const byModel = new Map(diagnoses.map((d) => [d.model, d]));
|
|
681
777
|
for (const m of registry.list()) {
|
|
682
778
|
const tiers = registry.getTiers(m).join('/');
|
|
683
779
|
const est = registry.estimateCost(m, 1_000_000, 1_000_000);
|
|
684
780
|
const cost = est === null ? 'unpriced' : `$${est.toFixed(2)}/M`;
|
|
685
|
-
const
|
|
686
|
-
const avail =
|
|
781
|
+
const d = byModel.get(m);
|
|
782
|
+
const avail = d ? modelStatusIcon(d) : styleText('red', '❌');
|
|
687
783
|
const tag = registry.isExperimental(m) ? styleText('yellow', ' [experimental]') : '';
|
|
688
784
|
console.log(` ${avail} ${m} tier=${tiers} ${cost}${tag}`);
|
|
689
785
|
}
|
|
@@ -1663,6 +1759,7 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1663
1759
|
},
|
|
1664
1760
|
localOnly: Boolean(ctx?.localOnly),
|
|
1665
1761
|
blockUnresolvedRegressions: resolveDesignRegressionBlock(factoryConfig),
|
|
1762
|
+
readinessUpdateIssueBody: resolveReadinessUpdateIssueBody(factoryConfig),
|
|
1666
1763
|
publishFromBuild: resolveBuildPublish(factoryConfig),
|
|
1667
1764
|
sizeGateMode,
|
|
1668
1765
|
maxSlices,
|
|
@@ -1705,7 +1802,8 @@ export async function shipIssue(issueNum, opts, ctx, deps = {}) {
|
|
|
1705
1802
|
resolveConstitution: () =>
|
|
1706
1803
|
// Resolved once here — runIssue calls this exactly once and reuses the value for
|
|
1707
1804
|
// every phase, so the build worker can never author the standards it is graded by.
|
|
1708
|
-
|
|
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),
|
|
1709
1807
|
createApprovalGate: () => createFileApprovalGate({ dir: paths.approvals, timeoutMs: timeouts.approval * 1000 }),
|
|
1710
1808
|
drainSteering: () => drainSteering(paths.steering, issueNum, worktree),
|
|
1711
1809
|
reworkHistory,
|
|
@@ -2195,8 +2293,7 @@ async function cmdLocalSmallOvernight(opts) {
|
|
|
2195
2293
|
if (!coreOps.isCommandAvailable('claude'))
|
|
2196
2294
|
return { ok: false, reason: missingClaudeCliMessage() };
|
|
2197
2295
|
const registry = new coreOps.ModelRegistry(applyRepoConfig(configLoaders.loadModelsConfig(), configLoaders.loadRepoConfig(repoRoot)));
|
|
2198
|
-
const
|
|
2199
|
-
const diagnoses = coreOps.diagnoseModels(registry, { ollamaModelPresent: ollamaModels ? (m) => ollamaModels.has(m) : undefined }, resolveExperimental(), true);
|
|
2296
|
+
const diagnoses = coreOps.diagnoseModels(registry, modelDoctorProbes(), resolveExperimental(), true);
|
|
2200
2297
|
if (!hasReachableWorker(diagnoses)) {
|
|
2201
2298
|
const reasons = diagnoses.filter((d) => !d.reachable).map((d) => `${d.model}: ${d.reason}`);
|
|
2202
2299
|
return { ok: false, reason: `no reachable local worker model — ${reasons.join('; ')}` };
|
|
@@ -2538,8 +2635,12 @@ async function cmdTriage(opts) {
|
|
|
2538
2635
|
const routesConfig = configLoaders.loadRoutesConfig();
|
|
2539
2636
|
const effective = resolveEffectiveConfig(repoConfig);
|
|
2540
2637
|
const router = new coreOps.ModelRouter(modelsConfig, routesConfig, false, undefined, effective.allowExperimental, effective.localOnly);
|
|
2541
|
-
|
|
2542
|
-
const
|
|
2638
|
+
// Triage always runs `claude -p`, so only claude-cli models with a flag are runnable.
|
|
2639
|
+
const registry = router.registryRef;
|
|
2640
|
+
const resolved = router.resolveAll('triage');
|
|
2641
|
+
const candidates = resolved.length > 0 ? resolved : ['claude-sonnet-5'];
|
|
2642
|
+
const model = candidates.find((m) => registry.getHarnessId(m) === 'claude-cli' && registry.getClaudeFlag(m));
|
|
2643
|
+
const flag = model ? registry.getClaudeFlag(model) : undefined;
|
|
2543
2644
|
const constitutionNote = product ? `Active constitution: ${product}.` : '';
|
|
2544
2645
|
const prompt = `Triage the open GitHub issues of ${ghRepo} for an autonomous software factory.
|
|
2545
2646
|
${constitutionNote}
|
|
@@ -2549,7 +2650,7 @@ and anything too vague. Group by lane (same-file issues together). Order by depe
|
|
|
2549
2650
|
Write ONLY the queue to ${paths.queueProposed} in format '<lane> <issue#>', with '#' comments
|
|
2550
2651
|
explaining exclusions.`;
|
|
2551
2652
|
let plannerError;
|
|
2552
|
-
logEvent(paths.events, 'triage', '-', `Triaging ${ghRepo} with ${model}`);
|
|
2653
|
+
logEvent(paths.events, 'triage', '-', `Triaging ${ghRepo} with ${flag ?? 'claude default model'}`);
|
|
2553
2654
|
await shellOps
|
|
2554
2655
|
.exec(`claude -p ${shellEscape(prompt)} ${flag ? `--model ${flag}` : ''} --allowedTools "Bash(gh issue:*)" "Bash(gh repo:*)" Read Glob Grep Write`)
|
|
2555
2656
|
.catch((err) => {
|
|
@@ -2960,7 +3061,19 @@ async function cmdFactoryd(opts) {
|
|
|
2960
3061
|
for (const notice of config.notices)
|
|
2961
3062
|
log(notice);
|
|
2962
3063
|
const updateFile = updateStatePath(runtime.dir);
|
|
2963
|
-
const
|
|
3064
|
+
const previousUpdate = readUpdateState(updateFile);
|
|
3065
|
+
for (const stop of recoverStaleDrain(previousUpdate, (file) => {
|
|
3066
|
+
try {
|
|
3067
|
+
unlinkSync(file);
|
|
3068
|
+
return true;
|
|
3069
|
+
}
|
|
3070
|
+
catch {
|
|
3071
|
+
return false;
|
|
3072
|
+
}
|
|
3073
|
+
})) {
|
|
3074
|
+
log(`update: removed stale STOP ${stop}`);
|
|
3075
|
+
}
|
|
3076
|
+
const updateState = initialUpdateState(config, previousUpdate);
|
|
2964
3077
|
try {
|
|
2965
3078
|
writeUpdateState(updateFile, updateState);
|
|
2966
3079
|
}
|
|
@@ -2992,8 +3105,26 @@ async function cmdFactoryd(opts) {
|
|
|
2992
3105
|
console.log(line);
|
|
2993
3106
|
for (const line of banner)
|
|
2994
3107
|
logSink(line);
|
|
3108
|
+
let shuttingDown = false;
|
|
3109
|
+
void runAutoUpdateLoop(config, {
|
|
3110
|
+
...createAutoUpdateDeps({
|
|
3111
|
+
registryFile,
|
|
3112
|
+
stateFile: updateFile,
|
|
3113
|
+
cliEntrypoint: fileURLToPath(new URL('../cli.js', import.meta.url)),
|
|
3114
|
+
check: () => checkForUpdate(config.autoUpdate.mode === 'prerelease' ? 'prerelease' : 'stable'),
|
|
3115
|
+
runRuntime,
|
|
3116
|
+
log,
|
|
3117
|
+
exit: async (code) => {
|
|
3118
|
+
await daemon.stop().catch(() => { });
|
|
3119
|
+
await releaseRuntimeFiles(runtime);
|
|
3120
|
+
process.exit(code);
|
|
3121
|
+
},
|
|
3122
|
+
}),
|
|
3123
|
+
stopped: () => shuttingDown,
|
|
3124
|
+
});
|
|
2995
3125
|
await new Promise((resolveShutdown) => {
|
|
2996
3126
|
const shutdown = () => {
|
|
3127
|
+
shuttingDown = true;
|
|
2997
3128
|
void daemon
|
|
2998
3129
|
.stop()
|
|
2999
3130
|
.catch(() => { })
|
|
@@ -4275,14 +4406,17 @@ async function cmdDoctor(opts = {}) {
|
|
|
4275
4406
|
}
|
|
4276
4407
|
let sandboxed = 'skipped';
|
|
4277
4408
|
let sandboxDetail = 'skipped — host claude auth was not verified';
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4409
|
+
const runtimeAvailable = hasSandboxRuntime(process.platform, (cmd) => isCommandAvailableViaShell(shellOps, cmd));
|
|
4410
|
+
const skip = sandboxAuthSkipDetail({ runtimeAvailable, hostOk: host === 'ok', inRepo: repoRoot !== null });
|
|
4411
|
+
if (skip !== null) {
|
|
4412
|
+
sandboxDetail = skip;
|
|
4413
|
+
}
|
|
4414
|
+
else if (repoRoot !== null) {
|
|
4415
|
+
const policy = resolveSandboxPolicy(configLoaders.loadFactoryConfigForRepo(getFactoryPaths(repoRoot).config).sandbox, {
|
|
4416
|
+
worktree: repoRoot,
|
|
4417
|
+
repoRoot,
|
|
4418
|
+
isAvailable: (cmd) => isCommandAvailableViaShell(shellOps, cmd),
|
|
4419
|
+
});
|
|
4286
4420
|
if (!policy) {
|
|
4287
4421
|
sandboxDetail = 'skipped — sandbox disabled by config or FACTORY_SANDBOX';
|
|
4288
4422
|
}
|
|
@@ -4321,6 +4455,15 @@ async function cmdDoctor(opts = {}) {
|
|
|
4321
4455
|
checks.push(stopSentinelCheck(readStopFileStatus(paths)));
|
|
4322
4456
|
const eventsContent = existsSync(paths.events) ? readFileSync(paths.events, 'utf-8') : null;
|
|
4323
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));
|
|
4324
4467
|
// Full run-lifecycle reap (#998): dead-run worktrees and their branches. Uses the same
|
|
4325
4468
|
// GitHub evidence and the same lock pairing as `factory worktree gc` (cmdWorktreeGc) —
|
|
4326
4469
|
// a merged or closed PR is what makes a worktree reapable.
|
|
@@ -4493,8 +4636,9 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4493
4636
|
.command('constitution')
|
|
4494
4637
|
.description('Manage product constitutions — .factory/constitution.md in this repo is the source of truth')
|
|
4495
4638
|
.option('--init [product]', 'Write .factory/constitution.md in this repo, scaffolded from the template (an optional name overrides the display name)')
|
|
4496
|
-
.option('--list', 'List available constitutions')
|
|
4497
|
-
.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')
|
|
4498
4642
|
.option('--force', 'With --init or --product, overwrite an existing .factory/constitution.md')
|
|
4499
4643
|
.action((opts) => cmdConstitution(opts));
|
|
4500
4644
|
program
|
|
@@ -4588,6 +4732,8 @@ export async function main(argv = process.argv, deps = {}) {
|
|
|
4588
4732
|
.command('feedback <pr-url>')
|
|
4589
4733
|
.description("File one factory issue (bug, factory:needs-triage) from a PR's human review findings")
|
|
4590
4734
|
.option('--note <text>', 'Use this as the issue title instead of the first finding')
|
|
4735
|
+
.option('--repo <owner/name>', 'Repo to file the issue in (default: filing.repo, then the origin remote)')
|
|
4736
|
+
.option('--include-local-paths', 'Include this host name and local design/log paths in the issue')
|
|
4591
4737
|
.option('--branch-prefix <prefix>', BRANCH_PREFIX_MATCH_OPTION_DESCRIPTION)
|
|
4592
4738
|
.action(cmdFeedback);
|
|
4593
4739
|
const filingCmd = program.command('filing').description('Inspect upstream factory reports (nothing is sent)');
|