mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Expand doctor operational diagnostics
This commit is contained in:
1 parent
6ac3dec1df
commit
f892ab2402
6 files changed
+105
-7
No files matched your search
@@ -26,7 +26,9 @@ and the first Claude reference-fleet path:
|
||||
- tracked `.torch/` project configuration;
|
||||
- XDG-local runtime state;
|
||||
- installation ownership manifest;
|
||||
- health diagnostics;
|
||||
- read-only health diagnostics covering installation integrity, runtime
|
||||
availability, worktree drift count and age, Git operations, and honest
|
||||
recoverability grade;
|
||||
- conservative, reversible purge;
|
||||
- reviewed worktree plans with branch/path collision protection;
|
||||
- isolated branches and worktrees with locally ignored task markers;
|
||||
|
||||
@@ -1126,6 +1126,13 @@ task/commit references, and verified item counts. Reports keep evidence classes
|
||||
separate, derive cache-read ratio and cost per verified item, and warn when
|
||||
usage has no verified outcome.
|
||||
|
||||
`torch doctor` additionally reports managed-worktree branch/commit consistency,
|
||||
ahead and behind counts, the timestamp and age of the oldest missing canonical
|
||||
commit, in-progress Git operations, configured runtime executable availability,
|
||||
and the current commit's `ONE-DISK`, `LOCAL-REMOTE`, or `OFF-MACHINE`
|
||||
recoverability grade. Each finding separates observed evidence from a
|
||||
recommended action; doctor remains read-only.
|
||||
|
||||
## 22. Scheduling
|
||||
|
||||
TORCH recognizes two schedule classes.
|
||||
|
||||
@@ -41,7 +41,7 @@ test_integrity_note:
|
||||
- id: SCN-worktree-bootstrap
|
||||
given: an approved TORCH organization committed to a clean main branch
|
||||
when: the owner previews and creates its managed worktrees
|
||||
then: every persistent identity receives the intended branch and external worktree, task markers are ignored, and repeated planning is idempotent
|
||||
then: every persistent identity receives the intended branch and external worktree, task markers are ignored, repeated planning is idempotent, and doctor reports later drift age, Git operations, runtime availability, and recoverability without repair
|
||||
- id: SCN-bootstrap-acceptance
|
||||
given: one approved three-identity fixture with checks, a scarce resource, and neighboring domains
|
||||
when: TORCH installs worktrees, starts virtualized sessions, coordinates messages, runs backlog/check/resource lifecycles, diagnoses, captures, stops, reopens, resumes, and previews reversal
|
||||
@@ -238,6 +238,7 @@ test_integrity_note:
|
||||
- Git status before and after read-only analysis
|
||||
- presence or absence of .torch and XDG-local project state
|
||||
- doctor health and finding codes
|
||||
- worktree ahead and behind counts, oldest-missing-commit age, active Git operations, runtime detection, and recoverability grade
|
||||
- CLI exit status and structured error code
|
||||
- proposal review status, repository commit binding, and generated domain count
|
||||
- specification digest, evidence-linked responsibility, and stale-spec refusal
|
||||
@@ -285,6 +286,7 @@ test_integrity_note:
|
||||
- init that silently writes files
|
||||
- installer that overwrites existing state
|
||||
- doctor that reports healthy without required state
|
||||
- doctor hiding convergence age, an active Git operation, a missing runtime, or one-disk-only recovery
|
||||
- uninstall that deletes owner-modified configuration
|
||||
- CLI that mutates without explicit approval
|
||||
- in-memory-only messaging that disappears on restart
|
||||
|
||||
+59
-2
@@ -6,12 +6,15 @@ import { projectStatePath } from './paths.mjs';
|
||||
import { inspectManagedWorktree } from './worktree-state.mjs';
|
||||
import { validateProjectConfig } from './config.mjs';
|
||||
import { forgeStatus } from '../forge/service.mjs';
|
||||
import { classifyRecoverability } from '../canonical/local.mjs';
|
||||
import { createClaudeAdapter } from '../adapters/claude.mjs';
|
||||
import { createCodexAdapter } from '../adapters/codex.mjs';
|
||||
|
||||
function parseJsonYaml(path) {
|
||||
return JSON.parse(readFileSync(path, 'utf8'));
|
||||
}
|
||||
|
||||
export function diagnoseProject({ repository, env = process.env }) {
|
||||
export function diagnoseProject({ repository, env = process.env, now = () => new Date() }) {
|
||||
const trackedRoot = join(repository.root, '.torch');
|
||||
const configPath = join(trackedRoot, 'torch.yaml');
|
||||
const manifestPath = join(trackedRoot, 'install-manifest.json');
|
||||
@@ -129,10 +132,28 @@ export function diagnoseProject({ repository, env = process.env }) {
|
||||
const state = inspectManagedWorktree(repository.root, entry, config?.project?.main_branch ?? 'main');
|
||||
for (const problem of state.problems) {
|
||||
findings.push({
|
||||
severity: problem === 'worktree-dirty' || problem.startsWith('unique-commits:') ? 'warning' : 'error',
|
||||
severity: problem === 'worktree-dirty' || problem.startsWith('unique-commits:')
|
||||
|| problem.startsWith('git-operation:') ? 'warning' : 'error',
|
||||
code: 'WORKTREE_PROBLEM', area: entry.area, path: entry.path, problem,
|
||||
});
|
||||
}
|
||||
if (state.ahead || state.behind) {
|
||||
const missingAt = state.oldestMissingCommitAt ? Date.parse(state.oldestMissingCommitAt) : null;
|
||||
findings.push({
|
||||
severity: 'info', code: 'WORKTREE_DRIFT', area: entry.area, path: entry.path,
|
||||
branch: state.branch, commit: state.commit, drift: state.drift,
|
||||
ahead: state.ahead, behind: state.behind,
|
||||
oldestMissingCommitAt: state.oldestMissingCommitAt,
|
||||
driftAgeSeconds: Number.isFinite(missingAt)
|
||||
? Math.max(0, Math.floor((now().getTime() - missingAt) / 1000)) : null,
|
||||
recommendation: 'Ask the owning domain to review an intentional convergence plan at a safe point.',
|
||||
});
|
||||
}
|
||||
if (state.operations.length) findings.push({
|
||||
severity: 'warning', code: 'GIT_OPERATION_ACTIVE', area: entry.area,
|
||||
path: entry.path, operations: state.operations,
|
||||
recommendation: 'The owning domain must complete or deliberately abort the Git operation.',
|
||||
});
|
||||
}
|
||||
const managerWorktree = (manifest.external ?? []).find((entry) =>
|
||||
entry.type === 'worktree' && entry.area === 'session-manager');
|
||||
@@ -169,6 +190,42 @@ export function diagnoseProject({ repository, env = process.env }) {
|
||||
if (repository.dirtyEntries.length) {
|
||||
findings.push({ severity: 'info', code: 'WORKTREE_DIRTY', count: repository.dirtyEntries.length });
|
||||
}
|
||||
try {
|
||||
const recoverability = classifyRecoverability({ repositoryRoot: repository.root, commit: repository.head });
|
||||
findings.push({
|
||||
severity: recoverability.offMachine ? 'info' : 'warning', code: 'RECOVERABILITY',
|
||||
...recoverability,
|
||||
recommendation: recoverability.offMachine
|
||||
? 'No action required for this commit.'
|
||||
: 'Configure and synchronize an off-machine canonical Git boundary before relying on this checkout for recovery.',
|
||||
});
|
||||
} catch (error) {
|
||||
findings.push({ severity: 'error', code: 'RECOVERABILITY_INVALID', message: error.message });
|
||||
}
|
||||
if (config?.runtimes) {
|
||||
const adapters = new Map([
|
||||
['claude', createClaudeAdapter({ env })],
|
||||
['codex', createCodexAdapter({ env })],
|
||||
]);
|
||||
for (const runtime of Object.keys(config.runtimes).filter((name) => name !== 'default')) {
|
||||
const adapter = adapters.get(runtime);
|
||||
if (!adapter) {
|
||||
findings.push({
|
||||
severity: 'warning', code: 'RUNTIME_ADAPTER_UNKNOWN', runtime,
|
||||
recommendation: 'Install or configure a TORCH runtime adapter before starting this identity.',
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const detection = adapter.detect();
|
||||
findings.push({
|
||||
severity: detection.available ? 'info' : 'warning',
|
||||
code: detection.available ? 'RUNTIME_AVAILABLE' : 'RUNTIME_UNAVAILABLE',
|
||||
runtime, executable: detection.executable,
|
||||
recommendation: detection.available ? 'No action required.'
|
||||
: `Install ${runtime} or change the approved runtime assignment before startup.`,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (config?.forge?.provider && config.forge.provider !== 'none') {
|
||||
try {
|
||||
const forge = forgeStatus({ repositoryRoot: repository.root });
|
||||
|
||||
@@ -14,14 +14,22 @@ export function inspectManagedWorktree(root, entry, mainBranch) {
|
||||
const problems = [];
|
||||
const actualBranch = git(entry.path, ['branch', '--show-current']);
|
||||
if (actualBranch !== entry.branch) problems.push(`branch-mismatch:${actualBranch || 'detached'}`);
|
||||
const operations = ['MERGE_HEAD', 'REBASE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD']
|
||||
.filter((name) => existsSync(git(entry.path, ['rev-parse', '--git-path', name])));
|
||||
for (const operation of operations) problems.push(`git-operation:${operation}`);
|
||||
const status = git(entry.path, ['status', '--porcelain=v1', '--untracked-files=all']);
|
||||
if (status) problems.push('worktree-dirty');
|
||||
const commit = git(entry.path, ['rev-parse', 'HEAD']);
|
||||
const ahead = Number(git(root, ['rev-list', '--count', `${mainBranch}..${entry.branch}`]) || 0);
|
||||
const behind = Number(git(root, ['rev-list', '--count', `${entry.branch}..${mainBranch}`]) || 0);
|
||||
const oldestMissingCommitAt = behind > 0
|
||||
? git(root, ['log', '--reverse', '--format=%cI', `${entry.branch}..${mainBranch}`]).split('\n')[0] || null
|
||||
: null;
|
||||
if (ahead > 0) problems.push(`unique-commits:${ahead}`);
|
||||
return {
|
||||
...entry, actualBranch, ahead, behind,
|
||||
drift: ahead === 0 && behind === 0 ? 'current' : 'diverged',
|
||||
...entry, actualBranch, commit, ahead, behind, oldestMissingCommitAt, operations,
|
||||
drift: ahead > 0 && behind > 0 ? 'diverged'
|
||||
: (ahead > 0 ? 'ahead' : (behind > 0 ? 'behind' : 'current')),
|
||||
safeToRemove: problems.length === 0, problems,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { existsSync, mkdtempSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { basename, join } from 'node:path';
|
||||
import test from 'node:test';
|
||||
@@ -53,6 +53,28 @@ test('SCN-worktree-bootstrap: approved identities receive isolated branches and
|
||||
assert.equal(repeated.actions.every((action) => action.action === 'keep'), true);
|
||||
assert.equal(repeated.canProceed, true);
|
||||
assert.equal(diagnoseProject({ repository: inspectRepository(fixture.root), env: fixture.env }).healthy, true);
|
||||
|
||||
writeFileSync(join(fixture.root, 'drift.txt'), 'new canonical state\n');
|
||||
execFileSync('git', ['-C', fixture.root, 'add', 'drift.txt']);
|
||||
execFileSync('git', ['-C', fixture.root, 'commit', '-m', 'advance canonical main']);
|
||||
const core = created.created.find((entry) => entry.area === 'core');
|
||||
const mergeHead = execFileSync('git', ['-C', core.path, 'rev-parse', '--git-path', 'MERGE_HEAD'], {
|
||||
encoding: 'utf8',
|
||||
}).trim();
|
||||
writeFileSync(mergeHead, `${execFileSync('git', ['-C', fixture.root, 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()}\n`);
|
||||
const diagnosis = diagnoseProject({
|
||||
repository: inspectRepository(fixture.root), env: fixture.env,
|
||||
now: () => new Date('2026-09-28T00:00:00Z'),
|
||||
});
|
||||
const drift = diagnosis.findings.find((finding) => finding.code === 'WORKTREE_DRIFT' && finding.area === 'core');
|
||||
assert.equal(drift.behind, 1);
|
||||
assert.equal(Number.isInteger(drift.driftAgeSeconds), true);
|
||||
assert.equal(diagnosis.findings.some((finding) =>
|
||||
finding.code === 'GIT_OPERATION_ACTIVE' && finding.operations.includes('MERGE_HEAD')), true);
|
||||
assert.equal(diagnosis.findings.some((finding) =>
|
||||
finding.code === 'RECOVERABILITY' && finding.level === 'ONE-DISK'), true);
|
||||
assert.equal(diagnosis.findings.some((finding) => finding.code.startsWith('RUNTIME_')), true);
|
||||
unlinkSync(mergeHead);
|
||||
uninstallProject({ repository: inspectRepository(fixture.root), purge: true });
|
||||
assert.equal(created.created.some((action) => existsSync(action.path)), false);
|
||||
assert.equal(readFileSync(join(fixture.root, '.git', 'info', 'exclude'), 'utf8').includes('.task'), false);
|
||||
|
||||
Reference in new issue
Block a user