@on-par/factory-cli 0.9.7-next.1 → 0.9.7-next.10
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/dist/cli/doctor-config.d.ts +14 -0
- package/dist/cli/doctor-config.d.ts.map +1 -0
- package/dist/cli/doctor-config.js +126 -0
- package/dist/cli/doctor-config.js.map +1 -0
- package/dist/cli/doctor-health.d.ts +73 -0
- package/dist/cli/doctor-health.d.ts.map +1 -0
- package/dist/cli/doctor-health.js +156 -0
- package/dist/cli/doctor-health.js.map +1 -0
- package/dist/cli/doctor.d.ts +26 -81
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +213 -267
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +18 -70
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +77 -811
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/run-lane.d.ts +52 -0
- package/dist/cli/run-lane.d.ts.map +1 -0
- package/dist/cli/run-lane.js +239 -0
- package/dist/cli/run-lane.js.map +1 -0
- package/dist/cli/run.d.ts +52 -0
- package/dist/cli/run.d.ts.map +1 -0
- package/dist/cli/run.js +200 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/ship.d.ts.map +1 -1
- package/dist/cli/ship.js.map +1 -1
- package/dist/cli/status.d.ts +23 -0
- package/dist/cli/status.d.ts.map +1 -0
- package/dist/cli/status.js +179 -0
- package/dist/cli/status.js.map +1 -0
- package/package.json +3 -3
package/dist/cli/doctor.js
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// packages/cli/src/cli/doctor.ts — `factory doctor` environment preflight
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
-
import { detectSandboxRuntime } from '@on-par/factory-core';
|
|
5
|
-
import {
|
|
4
|
+
import { applyRepoConfig, ConstitutionLoader, detectSandboxRuntime, getFactoryPaths, inspectPortLeases, KNOWN_HARNESS_IDS, loadFactoryConfig, reapStalePortLeases, resolveProcessGroupGraceMs, resolveSandboxPolicy, } from '@on-par/factory-core';
|
|
5
|
+
import { claudeStoredSessionState, codexChatgptSessionState, createOctokitQueueClient, hasClaudeLoginEvidence, hasOpencodeLoginEvidence, isHarnessCliPresent, localAuthFailureSinceLogin, logEvent, microVmName, resolveBranchPrefix, shellEscape, withFileLock, withGitLock, wrapCommandInSandbox, } from '@on-par/factory-core/internal';
|
|
6
6
|
import { MIN_NODE_MAJOR } from '../node-version.js';
|
|
7
|
+
import { configLintChecks, declaredHarnessChecks, modelsRegistryChecks } from './doctor-config.js';
|
|
8
|
+
import { analyzeEventLog, eventLogCheck, formatClaimReconcileReport, formatContainerReconcileReport, formatMicroVmReconcileReport, formatReconcileReport, formatWorktreeReconcileReport, leaseChecks, orphanContainerChecks, orphanMicroVmChecks, unmergedGreenPrChecks, } from './doctor-health.js';
|
|
7
9
|
import { availableAgentClis, CLAUDE_CODE_URL, CODEX_CLI_URL, GITHUB_TOKENS_URL } from './first-run.js';
|
|
10
|
+
import { distFreshnessProbe } from './staleness.js';
|
|
11
|
+
import { readStopFileStatus, stopSentinelCheck } from './stop-sentinel.js';
|
|
12
|
+
import { checkSweepHeartbeat, defaultSweepHeartbeatDeps, sweepHeartbeatCheck } from './sweep-heartbeat.js';
|
|
8
13
|
/** Section for rows about repo-configured command harnesses (#2691). */
|
|
9
14
|
export const COMMAND_HARNESS_SECTION = 'Command harness';
|
|
10
15
|
/** Repo wired-up rows, sectioned only inside a git repo (#2557). */
|
|
@@ -672,279 +677,220 @@ export function reconcileAgentAuthChecks(checks) {
|
|
|
672
677
|
: { ...c, optional: false };
|
|
673
678
|
});
|
|
674
679
|
}
|
|
675
|
-
export function
|
|
676
|
-
|
|
677
|
-
|
|
680
|
+
export async function cmdDoctor(opts, rt) {
|
|
681
|
+
let repoRoot;
|
|
682
|
+
try {
|
|
683
|
+
repoRoot = rt.shellOps
|
|
684
|
+
.execSync('git rev-parse --show-toplevel', {
|
|
685
|
+
timeout: 10_000,
|
|
686
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
687
|
+
})
|
|
688
|
+
.trim();
|
|
678
689
|
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
690
|
+
catch {
|
|
691
|
+
repoRoot = null;
|
|
692
|
+
}
|
|
693
|
+
const checks = runDoctorChecks({
|
|
694
|
+
commandAvailable: rt.coreOps.isCommandAvailable,
|
|
695
|
+
envPresent: (key) => !!process.env[key],
|
|
696
|
+
tryExec: (cmd, opts) => {
|
|
697
|
+
try {
|
|
698
|
+
return rt.shellOps
|
|
699
|
+
.execSync(cmd, {
|
|
700
|
+
timeout: opts?.timeoutMs ?? 10_000,
|
|
701
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
702
|
+
...(opts?.env ? { env: { ...process.env, ...opts.env } } : {}),
|
|
703
|
+
})
|
|
704
|
+
.trim();
|
|
705
|
+
}
|
|
706
|
+
catch {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
pathExists: existsSync,
|
|
711
|
+
distFreshness: distFreshnessProbe(import.meta.url),
|
|
712
|
+
codexSessionState: () => codexChatgptSessionState(),
|
|
713
|
+
claudeLoggedIn: () => hasClaudeLoginEvidence({
|
|
714
|
+
exec: (file, args) => {
|
|
715
|
+
rt.shellOps.execSync([file, ...args].map(shellEscape).join(' '), { timeout: 10_000, stdio: 'ignore' });
|
|
716
|
+
},
|
|
717
|
+
fileExists: existsSync,
|
|
718
|
+
}),
|
|
719
|
+
claudeSessionState: () => claudeStoredSessionState(),
|
|
720
|
+
localAuthFailure: (agent) => localAuthFailureSinceLogin(agent),
|
|
721
|
+
opencodeLoggedIn: () => hasOpencodeLoginEvidence(),
|
|
722
|
+
commandHarnesses: () => repoRoot === null
|
|
723
|
+
? []
|
|
724
|
+
: repoCommandHarnesses(rt.configLoaders.loadModelsConfig().harnesses, rt.configLoaders.loadRepoConfig(repoRoot)?.models?.registry),
|
|
696
725
|
});
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
726
|
+
// Only with --probe-inference: these send a real prompt (uses quota). Plain doctor makes no inference call.
|
|
727
|
+
if (opts.probeInference) {
|
|
728
|
+
const CLAUDE_AUTH_PROBE = 'claude -p "reply with exactly: ok"';
|
|
729
|
+
const probeExec = (cmd) => {
|
|
730
|
+
try {
|
|
731
|
+
return rt.shellOps.execSync(cmd, { timeout: 120_000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
return null;
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
let host = 'skipped';
|
|
738
|
+
let hostDetail = 'skipped: claude CLI not on PATH';
|
|
739
|
+
if (rt.coreOps.isCommandAvailable('claude')) {
|
|
740
|
+
host = probeExec(CLAUDE_AUTH_PROBE) === null ? 'failed' : 'ok';
|
|
741
|
+
hostDetail = host === 'ok' ? 'claude -p succeeded on the host' : 'claude -p failed on the host';
|
|
742
|
+
}
|
|
743
|
+
let sandboxed = 'skipped';
|
|
744
|
+
let sandboxDetail = 'skipped: host claude auth was not verified';
|
|
745
|
+
const runtimeAvailable = hasSandboxRuntime(process.platform, (cmd) => rt.isCommandAvailableViaShell(rt.shellOps, cmd));
|
|
746
|
+
const skip = sandboxAuthSkipDetail({ runtimeAvailable, hostOk: host === 'ok', inRepo: repoRoot !== null });
|
|
747
|
+
if (skip !== null) {
|
|
748
|
+
sandboxDetail = skip;
|
|
749
|
+
}
|
|
750
|
+
else if (repoRoot !== null) {
|
|
751
|
+
const policy = resolveSandboxPolicy(rt.configLoaders.loadFactoryConfigForRepo(getFactoryPaths(repoRoot).config).sandbox, {
|
|
752
|
+
worktree: repoRoot,
|
|
753
|
+
repoRoot,
|
|
754
|
+
isAvailable: (cmd) => rt.isCommandAvailableViaShell(rt.shellOps, cmd),
|
|
755
|
+
});
|
|
756
|
+
if (!policy) {
|
|
757
|
+
sandboxDetail = 'skipped: sandbox disabled by config or FACTORY_SANDBOX';
|
|
758
|
+
}
|
|
759
|
+
else if (policy.runtime === 'none') {
|
|
760
|
+
sandboxDetail = 'skipped: no sandbox runtime (sandbox-exec/firejail) on this host';
|
|
761
|
+
}
|
|
762
|
+
else if (policy.runtime === 'docker-sandbox') {
|
|
763
|
+
sandboxDetail = 'skipped: docker-sandbox containment is the microVM, not command wrapping (#653)';
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
sandboxed = probeExec(wrapCommandInSandbox(CLAUDE_AUTH_PROBE, policy)) === null ? 'failed' : 'ok';
|
|
767
|
+
sandboxDetail =
|
|
768
|
+
sandboxed === 'ok'
|
|
769
|
+
? `claude -p succeeded under ${policy.runtime}`
|
|
770
|
+
: `claude -p failed under ${policy.runtime} (local_auth)`;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
checks.push(...sandboxClaudeAuthChecks({ host, sandboxed, hostDetail, sandboxDetail }));
|
|
774
|
+
}
|
|
775
|
+
checks.push(claudeKeychainCheck(rt.probeClaudeKeychain()));
|
|
776
|
+
checks.splice(0, checks.length, ...reconcileAgentAuthChecks(checks));
|
|
777
|
+
if (repoRoot !== null) {
|
|
778
|
+
const paths = getFactoryPaths(repoRoot);
|
|
779
|
+
let factoryConfig;
|
|
702
780
|
try {
|
|
703
|
-
|
|
781
|
+
factoryConfig = rt.configLoaders.loadFactoryConfigForRepo(paths.config);
|
|
704
782
|
}
|
|
705
783
|
catch {
|
|
706
|
-
|
|
784
|
+
factoryConfig = loadFactoryConfig(); // schema errors are reported by the config row (#2611)
|
|
707
785
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
optional: true,
|
|
731
|
-
detail: `${integrity.unparseable} of ${integrity.total} lines unparseable (${pct}%)`,
|
|
732
|
-
fix: 'historical corruption from pre-lock parallel writes; appends are now serialized; metrics over this file carry this error bar',
|
|
733
|
-
};
|
|
734
|
-
}
|
|
735
|
-
export function orphanContainerChecks(names) {
|
|
736
|
-
if (names.length === 0) {
|
|
737
|
-
return [{ name: 'orphan containers', ok: true, detail: 'no orphan sf-job-*/factory.managed containers' }];
|
|
738
|
-
}
|
|
739
|
-
return names.map((name) => ({
|
|
740
|
-
name: `orphan container ${name}`,
|
|
741
|
-
ok: false,
|
|
742
|
-
detail: `${name} is stopped and not tied to any running job`,
|
|
743
|
-
fix: 'run `factory doctor --reconcile` to remove it and its anonymous volumes',
|
|
744
|
-
}));
|
|
745
|
-
}
|
|
746
|
-
export function orphanMicroVmChecks(names) {
|
|
747
|
-
if (names.length === 0) {
|
|
748
|
-
return [{ name: 'orphan sbx VMs', ok: true, detail: 'no orphan factory-* sbx VMs' }];
|
|
749
|
-
}
|
|
750
|
-
return names.map((name) => ({
|
|
751
|
-
name: `orphan sbx VM ${name}`,
|
|
752
|
-
ok: false,
|
|
753
|
-
detail: `${name} has no active lane holding its worktree`,
|
|
754
|
-
fix: 'run `factory doctor --reconcile` to remove it via `sbx rm --force`',
|
|
755
|
-
}));
|
|
756
|
-
}
|
|
757
|
-
export function formatContainerReconcileReport(reaped) {
|
|
758
|
-
if (reaped.length === 0)
|
|
759
|
-
return 'reconcile: no orphan containers';
|
|
760
|
-
return reaped
|
|
761
|
-
.map((r) => r.removed
|
|
762
|
-
? `reconcile: removed container ${r.name}`
|
|
763
|
-
: `reconcile: failed to remove container ${r.name}: ${r.detail}`)
|
|
764
|
-
.join('\n');
|
|
765
|
-
}
|
|
766
|
-
export function formatMicroVmReconcileReport(reaped) {
|
|
767
|
-
if (reaped.length === 0)
|
|
768
|
-
return 'reconcile: no orphan sbx VMs';
|
|
769
|
-
return reaped
|
|
770
|
-
.map((r) => r.removed
|
|
771
|
-
? `reconcile: removed sbx VM ${r.name} (sbx rm --force)`
|
|
772
|
-
: `reconcile: failed to remove sbx VM ${r.name}: ${r.detail}`)
|
|
773
|
-
.join('\n');
|
|
774
|
-
}
|
|
775
|
-
export function formatReconcileReport(reaped) {
|
|
776
|
-
if (reaped.length === 0)
|
|
777
|
-
return 'reconcile: no stale leases';
|
|
778
|
-
return reaped
|
|
779
|
-
.map((r) => `reconcile: freed port ${r.lease.port} (worktree ${r.lease.worktreeId}, reason ${r.reason})`)
|
|
780
|
-
.join('\n');
|
|
781
|
-
}
|
|
782
|
-
export function formatWorktreeReconcileReport(removed) {
|
|
783
|
-
if (removed.length === 0)
|
|
784
|
-
return 'reconcile: no dead-run worktrees';
|
|
785
|
-
return removed
|
|
786
|
-
.map((w) => {
|
|
787
|
-
const branchLabel = w.branch ?? 'detached';
|
|
788
|
-
const deleted = w.branchDeleted ? `, deleted branch ${w.branch}` : '';
|
|
789
|
-
return `reconcile: removed worktree ${w.path} (branch ${branchLabel}, reason ${w.reason})${deleted}`;
|
|
790
|
-
})
|
|
791
|
-
.join('\n');
|
|
792
|
-
}
|
|
793
|
-
export function formatClaimReconcileReport(released) {
|
|
794
|
-
if (released.length === 0)
|
|
795
|
-
return 'reconcile: no stale claims';
|
|
796
|
-
return released
|
|
797
|
-
.map((c) => c.released
|
|
798
|
-
? `reconcile: released issue #${c.issue} back to factory:queued (lease expired at ${c.expiresAt})`
|
|
799
|
-
: `reconcile: failed to release issue #${c.issue} (lease expired at ${c.expiresAt}): ${c.detail ?? 'unknown error'}`)
|
|
800
|
-
.join('\n');
|
|
801
|
-
}
|
|
802
|
-
const GREEN_PR_CHECK = 'green PRs awaiting merge';
|
|
803
|
-
export function unmergedGreenPrChecks(result) {
|
|
804
|
-
if (result.status === 'skipped') {
|
|
805
|
-
return [{ name: GREEN_PR_CHECK, ok: true, optional: true, detail: result.detail }];
|
|
806
|
-
}
|
|
807
|
-
if (result.status === 'failed') {
|
|
808
|
-
return [
|
|
809
|
-
{
|
|
810
|
-
name: GREEN_PR_CHECK,
|
|
811
|
-
ok: false,
|
|
812
|
-
optional: true,
|
|
813
|
-
detail: `scan failed: ${result.detail}`,
|
|
814
|
-
fix: 'check `gh auth status` and network access, then re-run `factory doctor`',
|
|
815
|
-
},
|
|
816
|
-
];
|
|
817
|
-
}
|
|
818
|
-
if (result.rows.length === 0) {
|
|
819
|
-
return [{ name: GREEN_PR_CHECK, ok: true, optional: true, detail: 'no green-and-ready PRs awaiting merge' }];
|
|
820
|
-
}
|
|
821
|
-
return result.rows.map((row) => ({
|
|
822
|
-
name: `green PR #${row.number}`,
|
|
823
|
-
ok: false,
|
|
824
|
-
optional: true,
|
|
825
|
-
detail: `${row.issue === null ? 'owning issue unknown' : `issue #${row.issue}`}: green and ready on ${row.branch}, open ${row.ageHours}h, not merged`,
|
|
826
|
-
fix: `review and merge #${row.number} (or close it); the factory never auto-merges an orphaned PR`,
|
|
827
|
-
}));
|
|
828
|
-
}
|
|
829
|
-
/** One doctor check per user-declared harness, from its `probe` result. */
|
|
830
|
-
export function declaredHarnessChecks(probes) {
|
|
831
|
-
return probes.map((p) => {
|
|
832
|
-
const name = `harness ${p.harness}`;
|
|
833
|
-
if (p.status === 'available')
|
|
834
|
-
return { name, ok: true, detail: `available (probe: ${p.probe})` };
|
|
835
|
-
if (p.status === 'unavailable') {
|
|
836
|
-
return {
|
|
837
|
-
name,
|
|
838
|
-
ok: false,
|
|
839
|
-
detail: `unavailable: probe \`${p.probe}\` failed: ${p.detail}`,
|
|
840
|
-
fix: 'install the agent CLI on PATH or fix the "probe" command in the models config harnesses section',
|
|
841
|
-
};
|
|
786
|
+
const health = await inspectPortLeases({ registryFile: paths.ports });
|
|
787
|
+
checks.push(...leaseChecks(health.map(rt.toLeaseRow)));
|
|
788
|
+
const dockerAvailable = rt.coreOps.isCommandAvailable('docker');
|
|
789
|
+
const orphanContainers = dockerAvailable ? await rt.internalOps.listOrphanContainers().catch(() => []) : [];
|
|
790
|
+
checks.push(...orphanContainerChecks(orphanContainers.map((c) => c.name)));
|
|
791
|
+
const sbxAvailable = rt.coreOps.isCommandAvailable('sbx');
|
|
792
|
+
const activeVmNames = new Set(health.filter((h) => h.alive).map((h) => microVmName(h.lease.worktreeId)));
|
|
793
|
+
const orphanVmNames = sbxAvailable
|
|
794
|
+
? (await rt.internalOps.listMicroVms()).filter((n) => !activeVmNames.has(n))
|
|
795
|
+
: [];
|
|
796
|
+
checks.push(...orphanMicroVmChecks(orphanVmNames));
|
|
797
|
+
const sweepStatus = checkSweepHeartbeat(factoryConfig.sweep, process.env, defaultSweepHeartbeatDeps());
|
|
798
|
+
const sweepCheck = sweepHeartbeatCheck(sweepStatus);
|
|
799
|
+
if (sweepCheck)
|
|
800
|
+
checks.push(sweepCheck);
|
|
801
|
+
checks.push(stopSentinelCheck(readStopFileStatus(paths)));
|
|
802
|
+
const eventsContent = existsSync(paths.events) ? readFileSync(paths.events, 'utf-8') : null;
|
|
803
|
+
checks.push(eventLogCheck(eventsContent === null ? null : analyzeEventLog(eventsContent)));
|
|
804
|
+
let constitution = null;
|
|
805
|
+
let loadError;
|
|
806
|
+
try {
|
|
807
|
+
constitution = new ConstitutionLoader().loadRepoConstitution(repoRoot, factoryConfig.paths?.constitution);
|
|
842
808
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
ok: false,
|
|
846
|
-
optional: true,
|
|
847
|
-
detail: 'unverified: no probe configured',
|
|
848
|
-
fix: 'add a "probe" command (exit 0 = reachable) to the harness in the models config',
|
|
849
|
-
};
|
|
850
|
-
});
|
|
851
|
-
}
|
|
852
|
-
const REGISTRY_CHECK = 'models.registry';
|
|
853
|
-
/** One failed check per models.registry entry whose harness is missing or unknown;
|
|
854
|
-
* a single ok row when every entry is valid; nothing when there is no registry. */
|
|
855
|
-
export function modelsRegistryChecks(configPath, registry, validHarnesses) {
|
|
856
|
-
const entries = Object.entries(registry ?? {});
|
|
857
|
-
if (entries.length === 0)
|
|
858
|
-
return [];
|
|
859
|
-
const failed = [];
|
|
860
|
-
for (const [id, def] of entries) {
|
|
861
|
-
const field = `models.registry.${id}.harness`;
|
|
862
|
-
const name = `${REGISTRY_CHECK} ${id}`;
|
|
863
|
-
if (def.harness === undefined || def.harness.trim() === '') {
|
|
864
|
-
failed.push({
|
|
865
|
-
name,
|
|
866
|
-
ok: false,
|
|
867
|
-
detail: `${configPath}: ${field} is missing`,
|
|
868
|
-
fix: `set "harness" to one of: ${validHarnesses.join(', ')}`,
|
|
869
|
-
});
|
|
809
|
+
catch (err) {
|
|
810
|
+
loadError = `cannot parse constitution: ${err instanceof Error ? err.message : String(err)}`;
|
|
870
811
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
812
|
+
checks.push(...configLintChecks({ constitution, configFile: rt.repoConfigFileFor(repoRoot, paths.config) }, loadError));
|
|
813
|
+
try {
|
|
814
|
+
const declared = Object.keys(rt.configLoaders.loadModelsConfig().harnesses ?? {});
|
|
815
|
+
checks.push(...modelsRegistryChecks(paths.config, rt.configLoaders.loadRepoConfig(repoRoot)?.models?.registry, [
|
|
816
|
+
...KNOWN_HARNESS_IDS,
|
|
817
|
+
...declared,
|
|
818
|
+
]));
|
|
878
819
|
}
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
fix: 'fix the constitution frontmatter YAML',
|
|
903
|
-
},
|
|
904
|
-
...schemaRows,
|
|
905
|
-
];
|
|
906
|
-
}
|
|
907
|
-
const { findings } = lintFactoryConfig(input);
|
|
908
|
-
if (findings.length > 0) {
|
|
909
|
-
const names = [...listBuiltInCheckers().map((c) => c.name), 'custom_*'].join(', ');
|
|
910
|
-
const lintRows = findings.map((f) => {
|
|
911
|
-
const where = `${f.file}${f.line ? `:${f.line}` : ''}`;
|
|
912
|
-
if (f.rule === 'unknown-checker') {
|
|
913
|
-
const kind = isTemplateToken(f.token) ? 'placeholder' : 'unknown';
|
|
914
|
-
return {
|
|
915
|
-
name: 'config',
|
|
916
|
-
ok: false,
|
|
917
|
-
section: REPO_SECTION,
|
|
918
|
-
detail: `${where}: ${kind} checker '${f.token}'`,
|
|
919
|
-
fix: `replace '${f.token}' with a real checker name (${names}) or delete the line`,
|
|
920
|
-
};
|
|
820
|
+
catch {
|
|
821
|
+
// malformed config is reported by the load paths / `factory models --doctor`; doctor keeps going
|
|
822
|
+
}
|
|
823
|
+
let harnessProbes = [];
|
|
824
|
+
try {
|
|
825
|
+
const registry = new rt.coreOps.ModelRegistry(applyRepoConfig(rt.configLoaders.loadModelsConfig(), rt.configLoaders.loadRepoConfig(repoRoot)));
|
|
826
|
+
harnessProbes = rt.coreOps.probeDeclaredHarnesses(registry);
|
|
827
|
+
}
|
|
828
|
+
catch {
|
|
829
|
+
// models config problems are reported by `factory models --doctor`; doctor keeps going
|
|
830
|
+
}
|
|
831
|
+
checks.push(...declaredHarnessChecks(harnessProbes));
|
|
832
|
+
// Full run-lifecycle reap (#998): dead-run worktrees and their branches. Uses the same
|
|
833
|
+
// GitHub evidence and the same lock pairing as `factory worktree gc` (cmdWorktreeGc) —
|
|
834
|
+
// a merged or closed PR is what makes a worktree reapable.
|
|
835
|
+
const ghRepo = await rt.getGitHubRepo().catch(() => undefined);
|
|
836
|
+
const octokit = ghRepo && rt.hasGitHubToken() ? rt.getOctokit() : undefined;
|
|
837
|
+
// Green-and-ready PRs with no merge (#1000). Report only — doctor never lands a PR.
|
|
838
|
+
checks.push(...unmergedGreenPrChecks(await rt.scanGreenPrs(ghRepo, octokit)));
|
|
839
|
+
if (opts.reconcile) {
|
|
840
|
+
if (orphanContainers.length > 0) {
|
|
841
|
+
const reapedContainers = await rt.internalOps.reapOrphanContainers(orphanContainers);
|
|
842
|
+
console.log(formatContainerReconcileReport(reapedContainers));
|
|
921
843
|
}
|
|
922
|
-
|
|
923
|
-
name
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
844
|
+
if (orphanVmNames.length > 0) {
|
|
845
|
+
const reapedVms = await Promise.all(orphanVmNames.map((name) => rt.internalOps.reapOrphanMicroVm(name)));
|
|
846
|
+
console.log(formatMicroVmReconcileReport(reapedVms));
|
|
847
|
+
}
|
|
848
|
+
const reaped = await reapStalePortLeases({ registryFile: paths.ports, lockDir: paths.portsLock });
|
|
849
|
+
console.log(formatReconcileReport(reaped));
|
|
850
|
+
if (reaped.length > 0) {
|
|
851
|
+
const graceMs = resolveProcessGroupGraceMs(factoryConfig);
|
|
852
|
+
const orphanEvents = await rt.coreOps.reapOrphanProcesses({ reaped, graceMs });
|
|
853
|
+
for (const e of orphanEvents) {
|
|
854
|
+
console.log(`reconcile: ${e.action === 'killed' ? 'killed' : 'found'} pid ${e.pid} (pgid ${e.pgid}, ${e.command}) squatting port ${e.port} of dead lane ${e.worktreeId}${e.action === 'reported' ? '; not factory-started, left running' : ''}`);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
try {
|
|
858
|
+
const gcLog = (type, msg) => logEvent(paths.events, type, '-', msg);
|
|
859
|
+
const gcSandbox = rt.gcWorktreeSandbox(factoryConfig.sandbox, repoRoot);
|
|
860
|
+
const report = await withGitLock(repoRoot, () => withFileLock(paths.gitLock, () => rt.internalOps.sweepWorktrees({
|
|
861
|
+
repoRoot,
|
|
862
|
+
ttlDays: factoryConfig.worktree.gcTtlDays,
|
|
863
|
+
repo: ghRepo,
|
|
864
|
+
branchPrefix: resolveBranchPrefix(),
|
|
865
|
+
}, { log: gcLog, octokit, sandbox: gcSandbox })));
|
|
866
|
+
console.log(formatWorktreeReconcileReport(report.removed));
|
|
867
|
+
}
|
|
868
|
+
catch (err) {
|
|
869
|
+
console.log(`reconcile: worktree sweep failed: ${err?.message ?? String(err)}`);
|
|
870
|
+
}
|
|
871
|
+
// Stale claim reap (#999): factory:in-progress claims naming a dead local pid go back to
|
|
872
|
+
// factory:queued so the next run can claim them with no manual label surgery.
|
|
873
|
+
try {
|
|
874
|
+
if (ghRepo && octokit) {
|
|
875
|
+
const [owner, repoName] = ghRepo.split('/');
|
|
876
|
+
const released = await rt.internalOps.releaseStaleClaims({
|
|
877
|
+
client: createOctokitQueueClient(octokit),
|
|
878
|
+
owner,
|
|
879
|
+
repo: repoName,
|
|
880
|
+
});
|
|
881
|
+
console.log(formatClaimReconcileReport(released));
|
|
882
|
+
}
|
|
883
|
+
else {
|
|
884
|
+
console.log('reconcile: skipping stale claims: no GitHub repo or token');
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
catch (err) {
|
|
888
|
+
console.log(`reconcile: stale claim release failed: ${err?.message ?? String(err)}`);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
console.log(formatDoctorChecks(checks, { inRepo: repoRoot !== null }));
|
|
893
|
+
if (doctorFailed(checks))
|
|
894
|
+
process.exit(1);
|
|
949
895
|
}
|
|
950
896
|
//# sourceMappingURL=doctor.js.map
|