From 1252a40c60bae973710cbd95994e6e2b26168b73 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 14:37:12 -0400 Subject: [PATCH 01/12] feat(coding): add public Codex runtime authority --- modules/jarvos-coding/src/index.js | 4 + modules/jarvos-coding/src/runtime/codex.js | 45 +++++++++ .../src/runtime/repository-registry.js | 96 +++++++++++++++++++ .../jarvos-coding/test/codex-runtime.test.js | 41 ++++++++ 4 files changed, 186 insertions(+) create mode 100644 modules/jarvos-coding/src/runtime/codex.js create mode 100644 modules/jarvos-coding/src/runtime/repository-registry.js create mode 100644 modules/jarvos-coding/test/codex-runtime.test.js diff --git a/modules/jarvos-coding/src/index.js b/modules/jarvos-coding/src/index.js index f8ae90b6..046fb0b6 100644 --- a/modules/jarvos-coding/src/index.js +++ b/modules/jarvos-coding/src/index.js @@ -162,6 +162,8 @@ const workflowProvider = require('./providers/workflow-provider'); const learningEligibility = require('./providers/learning-eligibility'); const workRunStore = require('./features/work-run-store'); const managedWorkflow = require('./features/workflow'); +const codexRuntime = require('./runtime/codex'); +const repositoryRegistry = require('./runtime/repository-registry'); module.exports = { ...projectsActivity, ...stewardshipContract, @@ -173,6 +175,8 @@ module.exports = { ...learningEligibility, ...workRunStore, ...managedWorkflow, + ...codexRuntime, + ...repositoryRegistry, ACTIVE_STATUSES, DEFAULT_IGNORED_PATH_SEGMENTS, BRANCH_SCHEMA_VERSION, diff --git a/modules/jarvos-coding/src/runtime/codex.js b/modules/jarvos-coding/src/runtime/codex.js new file mode 100644 index 00000000..2c2790eb --- /dev/null +++ b/modules/jarvos-coding/src/runtime/codex.js @@ -0,0 +1,45 @@ +'use strict'; + +const crypto = require('node:crypto'); +const { buildLiveCodingAdapters } = require('../adapters/live'); +const { createFileWorkRunStore } = require('../features/work-run-store'); +const { loadRepositoryRegistry } = require('./repository-registry'); + +const CODEX_RUNTIME_SCHEMA_VERSION = 'jarvos-coding-codex-runtime/v1'; +const SUBJECT = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/; +const OPAQUE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +function digest(value) { return crypto.createHash('sha256').update(value).digest('hex'); } +function canonicalWorktree(entry, requested) { + // Worktree selection is controller-owned. This U1 boundary intentionally + // does not interpret a caller's filesystem path, even if it happens to be + // inside a registered root. + if (requested !== undefined && requested !== null) throw new Error('model-supplied worktree paths are not accepted'); + return entry.root; +} +function createCodexRuntime(options = {}) { + const registry = options.registry || loadRepositoryRegistry(options.registryPath, { ownerUid: options.ownerUid }); + function resolveRequest(input = {}) { + if (!input || typeof input !== 'object') throw new Error('coding request must be an object'); + for (const key of ['root', 'repositoryRoot', 'stateRoot', 'registryPath', 'provider', 'executable', 'command', 'credential', 'credentialReferences']) { + if (Object.prototype.hasOwnProperty.call(input, key)) throw new Error(`model-supplied ${key} is not accepted`); + } + if (!OPAQUE_ID.test(input.repositoryId || '')) throw new Error('repositoryId is required'); + if (typeof input.subjectKey !== 'string' || !SUBJECT.test(input.subjectKey)) throw new Error('subjectKey must be a safe stable identifier'); + const repository = registry.resolve(input.repositoryId); + if (input.agentSelectable !== false && !repository.agentSelectable) throw new Error('repository is not agent-selectable'); + const workRunId = input.workRunId || `run_${digest(`${repository.repositoryId}\0${input.subjectKey}`).slice(0, 24)}`; + if (!OPAQUE_ID.test(workRunId)) throw new Error('workRunId must be opaque'); + const qualifiedSubject = `${repository.repositoryId}:${input.subjectKey}`; + const worktree = canonicalWorktree(repository, input.worktree); + const store = createFileWorkRunStore(repository.stateRoot); + const existing = store.getWorkRun(workRunId, { public: false }); + if (existing && existing.subjectKey !== qualifiedSubject) throw new Error('workRunId belongs to a different repository-qualified subject'); + if (existing?.canonicalWorktree && existing.canonicalWorktree !== worktree) throw new Error('work run canonical worktree no longer matches repository authority'); + return Object.freeze({ repository, repositoryId: repository.repositoryId, subjectKey: qualifiedSubject, workRunId, canonicalWorktree: worktree, store, + public: Object.freeze({ version: CODEX_RUNTIME_SCHEMA_VERSION, repository: { repositoryId: repository.repositoryId, label: repository.publicLabel }, subjectKey: qualifiedSubject, workRunId }), + adapters: buildLiveCodingAdapters({ ...(options.liveAdapters || {}), repoRootDir: repository.root, worktreeRoot: repository.worktreePolicy.root, repo: repository.tracker.repo }), + }); + } + return Object.freeze({ schemaVersion: CODEX_RUNTIME_SCHEMA_VERSION, resolveRequest, listRepositories: () => registry.listPublic(), health: () => ({ version: CODEX_RUNTIME_SCHEMA_VERSION, status: 'installed-but-unwired', registryGeneration: registry.generation, repositories: registry.listPublic() }) }); +} +module.exports = { CODEX_RUNTIME_SCHEMA_VERSION, createCodexRuntime }; diff --git a/modules/jarvos-coding/src/runtime/repository-registry.js b/modules/jarvos-coding/src/runtime/repository-registry.js new file mode 100644 index 00000000..c1e26fc2 --- /dev/null +++ b/modules/jarvos-coding/src/runtime/repository-registry.js @@ -0,0 +1,96 @@ +'use strict'; + +// The registry is deliberately a host-owned file. Nothing that crosses the +// coding-tool boundary (paths, executables, credentials, or provider names) +// is used to construct this authority. +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); + +const REPOSITORY_REGISTRY_SCHEMA_VERSION = 'jarvos-coding-repository-registry/v1'; +const OPAQUE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +const ABSOLUTE_PATH = /^(?:\/(?!\/)|[A-Za-z]:[\\/]|\\\\)/; +const SAFE_LABEL = /^[^\0\r\n]{1,160}$/; +const SECRET = /(?:\bBearer\s+|\bsk-[A-Za-z0-9_-]{8,}|\bxox[baprs]-|(?:api[_-]?key|token|secret|password)\s*[:=])/i; +const ACCEPTANCE_MODES = new Set(['human-evidence-required', 'agent-mediated-allowed']); + +function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } +function assertNoSecret(value, label = 'registry') { + if (typeof value === 'string' && SECRET.test(value)) throw new Error(`${label} must not contain a secret value`); + if (Array.isArray(value)) value.forEach((entry, index) => assertNoSecret(entry, `${label}[${index}]`)); + if (isObject(value)) Object.entries(value).forEach(([key, entry]) => assertNoSecret(entry, `${label}.${key}`)); +} +function canonical(input, label) { + if (typeof input !== 'string' || !ABSOLUTE_PATH.test(input) || input.includes('\0')) throw new Error(`${label} must be an absolute path`); + let resolved; + try { resolved = fs.realpathSync(input); } catch { throw new Error(`${label} must exist and resolve canonically`); } + return resolved; +} +function inside(parent, child) { return child === parent || child.startsWith(`${parent}${path.sep}`); } +function deriveRepositoryId(entry) { + const identity = `${entry.publicLabel || entry.label || ''}\0${entry.root || entry.repositoryRoot || ''}`; + return `repo_${crypto.createHash('sha256').update(identity).digest('hex').slice(0, 24)}`; +} +function publicRepository(entry) { + return { repositoryId: entry.repositoryId, label: entry.publicLabel, agentSelectable: entry.agentSelectable }; +} +function normalizeEntry(raw) { + if (!isObject(raw)) throw new Error('repository entry must be an object'); + const allowed = new Set(['repositoryId', 'id', 'publicLabel', 'agentSelectable', 'root', 'repositoryRoot', 'stateRoot', 'tracker', 'worktreePolicy', 'acceptancePolicy', 'providerEgressPolicy', 'credentialReferences', 'learning', 'learningPublicationTarget']); + for (const key of Object.keys(raw)) if (!allowed.has(key)) throw new Error(`repository entry.${key} is not allowed`); + assertNoSecret(raw); + const root = canonical(raw.root || raw.repositoryRoot, 'repository root'); + const stateRoot = canonical(raw.stateRoot, 'repository stateRoot'); + const repositoryId = raw.repositoryId || raw.id || deriveRepositoryId({ ...raw, root }); + if (!OPAQUE_ID.test(repositoryId)) throw new Error('repositoryId must be an opaque identifier'); + if (typeof raw.publicLabel !== 'string' || !SAFE_LABEL.test(raw.publicLabel) || SECRET.test(raw.publicLabel) || ABSOLUTE_PATH.test(raw.publicLabel)) throw new Error('repository publicLabel must be public-safe text'); + if (typeof raw.agentSelectable !== 'boolean') throw new Error('repository agentSelectable must be boolean'); + if (!isObject(raw.worktreePolicy)) throw new Error('repository worktreePolicy is required'); + const worktreeRoot = canonical(raw.worktreePolicy.root || raw.worktreePolicy.worktreeRoot, 'repository worktreePolicy.root'); + if (inside(root, stateRoot) || inside(root, worktreeRoot) || inside(stateRoot, root) || inside(worktreeRoot, root) || inside(stateRoot, worktreeRoot) || inside(worktreeRoot, stateRoot)) throw new Error('repository roots must not overlap'); + const acceptancePolicy = raw.acceptancePolicy || { mode: 'human-evidence-required' }; + if (!isObject(acceptancePolicy) || !ACCEPTANCE_MODES.has(acceptancePolicy.mode)) throw new Error('repository acceptancePolicy.mode is invalid'); + if (acceptancePolicy.evidenceFreshnessMs !== undefined && (!Number.isInteger(acceptancePolicy.evidenceFreshnessMs) || acceptancePolicy.evidenceFreshnessMs < 0)) throw new Error('repository acceptancePolicy.evidenceFreshnessMs is invalid'); + if (raw.providerEgressPolicy !== undefined && !isObject(raw.providerEgressPolicy)) throw new Error('repository providerEgressPolicy must be an object'); + if (raw.tracker !== undefined && !isObject(raw.tracker)) throw new Error('repository tracker must be an object'); + if (raw.credentialReferences !== undefined && !isObject(raw.credentialReferences)) throw new Error('repository credentialReferences must be an object'); + return Object.freeze({ + repositoryId, publicLabel: raw.publicLabel, agentSelectable: raw.agentSelectable, + root, stateRoot, tracker: raw.tracker || {}, + worktreePolicy: Object.freeze({ ...raw.worktreePolicy, root: worktreeRoot }), + acceptancePolicy: Object.freeze({ ...acceptancePolicy }), providerEgressPolicy: Object.freeze({ ...(raw.providerEgressPolicy || {}) }), + credentialReferences: Object.freeze({ ...(raw.credentialReferences || {}) }), + learning: Object.freeze({ ...(raw.learning || {}) }), learningPublicationTarget: raw.learningPublicationTarget || null, + }); +} +function validateRepositoryRegistry(registry) { + const errors = []; + if (!isObject(registry)) return { ok: false, errors: ['registry must be an object'] }; + for (const key of Object.keys(registry)) if (!new Set(['schemaVersion', 'generation', 'repositories']).has(key)) errors.push(`registry.${key} is not allowed`); + if (registry.schemaVersion !== REPOSITORY_REGISTRY_SCHEMA_VERSION) errors.push(`registry.schemaVersion must be ${REPOSITORY_REGISTRY_SCHEMA_VERSION}`); + if (!Number.isInteger(registry.generation) || registry.generation < 1) errors.push('registry.generation must be a positive integer'); + if (!Array.isArray(registry.repositories)) errors.push('registry.repositories must be an array'); + const entries = []; + for (const raw of registry.repositories || []) { try { entries.push(normalizeEntry(raw)); } catch (error) { errors.push(error.message); } } + const ids = new Set(); + for (const entry of entries) { if (ids.has(entry.repositoryId)) errors.push(`duplicate repositoryId ${entry.repositoryId}`); ids.add(entry.repositoryId); } + return { ok: errors.length === 0, errors, entries }; +} +function loadRepositoryRegistry(registryPath, options = {}) { + if (typeof registryPath !== 'string' || !ABSOLUTE_PATH.test(registryPath)) throw new Error('registryPath must be an absolute host-bound path'); + const resolvedPath = canonical(registryPath, 'registryPath'); + const stat = fs.statSync(resolvedPath); + if (!stat.isFile()) throw new Error('registryPath must be a file'); + if ((stat.mode & 0o077) !== 0) throw new Error('registryPath permissions must not grant group or other access'); + if (options.ownerUid !== undefined && stat.uid !== options.ownerUid) throw new Error('registryPath is not owned by the expected owner'); + let parsed; try { parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8')); } catch (error) { throw new Error(`registryPath contains invalid JSON: ${error.message}`); } + const validation = validateRepositoryRegistry(parsed); + if (!validation.ok) throw new Error(`invalid repository registry: ${validation.errors.join('; ')}`); + const byId = new Map(validation.entries.map((entry) => [entry.repositoryId, entry])); + return Object.freeze({ schemaVersion: REPOSITORY_REGISTRY_SCHEMA_VERSION, generation: parsed.generation, path: resolvedPath, repositories: validation.entries, resolve(repositoryId) { + if (!OPAQUE_ID.test(repositoryId || '')) throw new Error('repositoryId must be an opaque identifier'); + const entry = byId.get(repositoryId); if (!entry) throw new Error('unknown repository'); return entry; + }, listPublic() { return validation.entries.filter((entry) => entry.agentSelectable).map(publicRepository); } }); +} + +module.exports = { REPOSITORY_REGISTRY_SCHEMA_VERSION, deriveRepositoryId, loadRepositoryRegistry, publicRepository, validateRepositoryRegistry }; diff --git a/modules/jarvos-coding/test/codex-runtime.test.js b/modules/jarvos-coding/test/codex-runtime.test.js new file mode 100644 index 00000000..619ba14f --- /dev/null +++ b/modules/jarvos-coding/test/codex-runtime.test.js @@ -0,0 +1,41 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const test = require('node:test'); +const { createCodexRuntime, REPOSITORY_REGISTRY_SCHEMA_VERSION } = require('../src'); + +function fixture() { + const base = fs.mkdtempSync(path.join(os.tmpdir(), 'jarvos-codex-runtime-')); + const root = path.join(base, 'repo'); const stateRoot = path.join(base, 'state'); const worktreeRoot = path.join(base, 'worktrees'); + for (const dir of [root, stateRoot, worktreeRoot]) fs.mkdirSync(dir); + const registryPath = path.join(base, 'registry.json'); + fs.writeFileSync(registryPath, JSON.stringify({ schemaVersion: REPOSITORY_REGISTRY_SCHEMA_VERSION, generation: 1, repositories: [{ repositoryId: 'repo_fixture', publicLabel: 'Fixture', agentSelectable: true, root, stateRoot, worktreePolicy: { root: worktreeRoot }, acceptancePolicy: { mode: 'human-evidence-required' }, providerEgressPolicy: {}, credentialReferences: {} }] })); + fs.chmodSync(registryPath, 0o600); return { root, stateRoot, worktreeRoot, registryPath }; +} +test('only owner-provisioned opaque repositories are publicly listed and resolved', () => { + const f = fixture(); const runtime = createCodexRuntime({ registryPath: f.registryPath }); + assert.deepEqual(runtime.listRepositories(), [{ repositoryId: 'repo_fixture', label: 'Fixture', agentSelectable: true }]); + const context = runtime.resolveRequest({ repositoryId: 'repo_fixture', subjectKey: 'ORG-1' }); + assert.equal(context.subjectKey, 'repo_fixture:ORG-1'); assert.equal(context.canonicalWorktree, fs.realpathSync(f.root)); + assert.doesNotMatch(JSON.stringify(context.public), /state|worktrees|\/(?:private|var)/); + assert.doesNotMatch(JSON.stringify(runtime.health()), new RegExp(f.root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); +}); +test('runtime fails closed for unknown ids, model paths, and cross-repository run reuse', () => { + const f = fixture(); const runtime = createCodexRuntime({ registryPath: f.registryPath }); + assert.throws(() => runtime.resolveRequest({ repositoryId: 'missing', subjectKey: 'ORG-1' }), /unknown repository/); + assert.throws(() => runtime.resolveRequest({ repositoryId: 'repo_fixture', subjectKey: 'ORG-1', worktree: f.root }), /model-supplied worktree/); + assert.throws(() => runtime.resolveRequest({ repositoryId: 'repo_fixture', subjectKey: 'ORG-1', root: f.root }), /model-supplied root/); + const context = runtime.resolveRequest({ repositoryId: 'repo_fixture', subjectKey: 'ORG-1', workRunId: 'run_fixed' }); + context.store.resolveWorkRun({ workRunId: context.workRunId, subjectKey: context.subjectKey, canonicalWorktree: context.canonicalWorktree }); + assert.throws(() => runtime.resolveRequest({ repositoryId: 'repo_fixture', subjectKey: 'ORG-2', workRunId: 'run_fixed' }), /different repository-qualified subject/); +}); +test('insecure registry files and overlapping authority roots are rejected before service', () => { + const f = fixture(); fs.chmodSync(f.registryPath, 0o644); + assert.throws(() => createCodexRuntime({ registryPath: f.registryPath }), /permissions/); + fs.chmodSync(f.registryPath, 0o600); + const registry = JSON.parse(fs.readFileSync(f.registryPath)); registry.repositories[0].stateRoot = f.root; fs.writeFileSync(f.registryPath, JSON.stringify(registry)); + assert.throws(() => createCodexRuntime({ registryPath: f.registryPath }), /must not overlap/); +}); From a3bc983904f32f7ca166fed762739890c0cca932 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 14:43:55 -0400 Subject: [PATCH 02/12] feat(coding): add owner repository provisioning --- lib/jarvos-cli.js | 95 ++++++++++++- .../src/runtime/repository-registry.js | 133 +++++++++++++++++- .../test/repository-provisioning.test.js | 81 +++++++++++ tests/cli-smoke-test.js | 31 ++++ 4 files changed, 337 insertions(+), 3 deletions(-) create mode 100644 modules/jarvos-coding/test/repository-provisioning.test.js diff --git a/lib/jarvos-cli.js b/lib/jarvos-cli.js index 9bebdc9b..0863aa33 100644 --- a/lib/jarvos-cli.js +++ b/lib/jarvos-cli.js @@ -6,10 +6,17 @@ const path = require('path'); const { spawnSync } = require('child_process'); const { loadHealthModules } = require('./jarvos-doctor-modules'); const { inspectCompoundEngineeringProvider } = require('../modules/jarvos-runtime-kit/src'); +const { + inspectProvisionedRepositories, + provisionRepository, + recordOwnerAction, + revokeProvisionedRepository, + updateProvisionedRepository, +} = require('../modules/jarvos-coding/src'); const ROOT = path.resolve(__dirname, '..'); const MIN_NODE_MAJOR = 18; -const COMMANDS = new Set(['help', 'init', 'doctor']); +const COMMANDS = new Set(['help', 'init', 'doctor', 'coding']); const LEGACY_INIT_ALIASES = new Set(['jarvos-bootstrap', 'jarvos-init']); const REQUIRED_WORKSPACE_FILES = [ 'AGENTS.md', @@ -530,6 +537,7 @@ function renderHelp() { Usage: jarvos init [--profile minimal] [bootstrap options] jarvos doctor [--profile minimal|local-openclaw|v0-5-0] [--workspace path] [--config path] [--json] + jarvos coding ... jarvos help Profiles: @@ -539,6 +547,82 @@ Compatibility: jarvos-bootstrap and jarvos-init route to jarvos init for one migration release.`; } +function renderCodingHelp() { + return `jarvos coding + +Owner-only repository provisioning. Every command requires an explicit --registry +path; jarvOS never infers repository authority from the current directory. + +Usage: + jarvos coding repository add --registry /absolute/registry.json --repository-json '{...}' [--json] + jarvos coding repository inspect --registry /absolute/registry.json [--json] + jarvos coding repository update --registry /absolute/registry.json --repository-id ID --repository-json '{...}' [--json] + jarvos coding repository revoke --registry /absolute/registry.json --repository-id ID [--json] + jarvos coding accept --registry /absolute/registry.json --repository-id ID --run-id ID --revision DIGEST [--json] + jarvos coding learning decline --registry /absolute/registry.json --repository-id ID --run-id ID [--json] + jarvos coding learning reset-retry --registry /absolute/registry.json --repository-id ID --run-id ID [--json] + +Repository JSON must supply explicit root, stateRoot, tracker, worktreePolicy, +acceptancePolicy, providerEgressPolicy, credentialReferences, learning, and +learningPublicationTarget. Registry data and owner-action records are restricted +to the invoking owner; normal receipts expose only public labels and opaque IDs.`; +} + +function parseCodingArgs(argv = []) { + const result = { positionals: [], options: {}, json: false }; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (value === '--help' || value === '-h') { result.help = true; continue; } + if (value === '--json') { result.json = true; continue; } + const match = value.match(/^--(registry|repository-id|repository-json|run-id|revision)=(.*)$/); + if (match) { result.options[match[1]] = match[2]; continue; } + if (['--registry', '--repository-id', '--repository-json', '--run-id', '--revision'].includes(value)) { + if (!argv[index + 1]) throw new Error(`${value} requires a value`); + result.options[value.slice(2)] = argv[++index]; continue; + } + if (value.startsWith('-')) throw new Error(`unknown coding option: ${value}`); + result.positionals.push(value); + } + return result; +} + +function renderCodingReceipt(receipt, json) { + if (json) return JSON.stringify(receipt, null, 2); + const repository = receipt.repository || receipt.repositories?.[0]; + const identity = repository ? ` ${repository.label || repository.publicLabel} (${repository.repositoryId})` : ''; + return `${receipt.action || 'inspected'} generation ${receipt.generation}${identity}`; +} + +function runCoding(argv = []) { + try { + const parsed = parseCodingArgs(argv); + if (parsed.positionals[0] === 'help' || parsed.positionals.length === 0 || parsed.help) { + process.stdout.write(`${renderCodingHelp()}\n`); return 0; + } + const [area, operation] = parsed.positionals; + const options = parsed.options; + let receipt; + if (area === 'repository') { + if (operation === 'inspect') receipt = inspectProvisionedRepositories({ registryPath: options.registry }); + else if (operation === 'add') receipt = provisionRepository({ registryPath: options.registry, repository: JSON.parse(options['repository-json'] || '') }); + else if (operation === 'update') receipt = updateProvisionedRepository({ registryPath: options.registry, repositoryId: options['repository-id'], repository: JSON.parse(options['repository-json'] || '') }); + else if (operation === 'revoke') receipt = revokeProvisionedRepository({ registryPath: options.registry, repositoryId: options['repository-id'] }); + else throw new Error('repository operation must be add, inspect, update, or revoke'); + } else if (area === 'accept' && operation === undefined) { + receipt = recordOwnerAction({ registryPath: options.registry, repositoryId: options['repository-id'], action: 'accept-plan', runId: options['run-id'], revision: options.revision }); + } else if (area === 'learning' && operation === 'decline') { + receipt = recordOwnerAction({ registryPath: options.registry, repositoryId: options['repository-id'], action: 'decline-learning', runId: options['run-id'] }); + } else if (area === 'learning' && operation === 'reset-retry') { + receipt = recordOwnerAction({ registryPath: options.registry, repositoryId: options['repository-id'], action: 'reset-learning-retry', runId: options['run-id'] }); + } else throw new Error('coding operation is not supported'); + process.stdout.write(`${renderCodingReceipt(receipt, parsed.json)}\n`); + return 0; + } catch (error) { + process.stderr.write(`jarvos coding failed: ${error.message}\n`); + return 1; + } +} + function renderInitHelp() { const profiles = listProfiles(); const profileLines = profiles.length @@ -636,7 +720,7 @@ async function runCli(argv = process.argv.slice(2), env = process.env, invokedAs const normalizedArgv = normalizeArgvForInvocation(argv, invokedAs); const parsed = parseArgs(normalizedArgv); if (parsed.command === 'help' - || parsed.help && !['doctor', 'init'].includes(parsed.command)) { + || parsed.help && !['doctor', 'init', 'coding'].includes(parsed.command)) { process.stdout.write(`${renderHelp()}\n`); return 0; } @@ -645,6 +729,10 @@ async function runCli(argv = process.argv.slice(2), env = process.env, invokedAs return runInit(normalizedArgv.slice(1), env); } + if (parsed.command === 'coding') { + return runCoding(normalizedArgv.slice(1)); + } + if (parsed.command === 'doctor') { if (parsed.help) { process.stdout.write(`${renderDoctorHelp()}\n`); @@ -700,6 +788,9 @@ module.exports = { parseArgs, renderDoctor, renderDoctorHelp, + renderCodingHelp, + parseCodingArgs, + runCoding, renderHelp, renderInitHelp, resolveDoctorContext, diff --git a/modules/jarvos-coding/src/runtime/repository-registry.js b/modules/jarvos-coding/src/runtime/repository-registry.js index c1e26fc2..3a32f7c2 100644 --- a/modules/jarvos-coding/src/runtime/repository-registry.js +++ b/modules/jarvos-coding/src/runtime/repository-registry.js @@ -13,6 +13,7 @@ const ABSOLUTE_PATH = /^(?:\/(?!\/)|[A-Za-z]:[\\/]|\\\\)/; const SAFE_LABEL = /^[^\0\r\n]{1,160}$/; const SECRET = /(?:\bBearer\s+|\bsk-[A-Za-z0-9_-]{8,}|\bxox[baprs]-|(?:api[_-]?key|token|secret|password)\s*[:=])/i; const ACCEPTANCE_MODES = new Set(['human-evidence-required', 'agent-mediated-allowed']); +const OWNER_ACTIONS_SCHEMA_VERSION = 'jarvos-coding-owner-actions/v1'; function isObject(value) { return value !== null && typeof value === 'object' && !Array.isArray(value); } function assertNoSecret(value, label = 'registry') { @@ -93,4 +94,134 @@ function loadRepositoryRegistry(registryPath, options = {}) { }, listPublic() { return validation.entries.filter((entry) => entry.agentSelectable).map(publicRepository); } }); } -module.exports = { REPOSITORY_REGISTRY_SCHEMA_VERSION, deriveRepositoryId, loadRepositoryRegistry, publicRepository, validateRepositoryRegistry }; +function assertOwner(stat, label, ownerUid = process.getuid?.()) { + if (ownerUid !== undefined && stat.uid !== ownerUid) throw new Error(`${label} is not owned by the current owner`); + if ((stat.mode & 0o077) !== 0) throw new Error(`${label} permissions must not grant group or other access`); +} +function requireProvisioningPath(value, label) { + if (typeof value !== 'string' || !ABSOLUTE_PATH.test(value) || value.includes('\0')) throw new Error(`${label} is required and must be an absolute path`); + return path.resolve(value); +} +function assertNotSymlink(value, label, required = true) { + if (!fs.existsSync(value)) { + if (required) throw new Error(`${label} must exist`); + return; + } + if (fs.lstatSync(value).isSymbolicLink()) throw new Error(`${label} must not be a symbolic link`); +} +function ensureOwnedDirectory(value, label, options = {}) { + const absolute = requireProvisioningPath(value, label); + assertNotSymlink(absolute, label, false); + fs.mkdirSync(absolute, { recursive: true, mode: 0o700 }); + const stat = fs.statSync(absolute); + if (!stat.isDirectory()) throw new Error(`${label} must be a directory`); + assertOwner(stat, label, options.ownerUid); + fs.chmodSync(absolute, 0o700); + return fs.realpathSync(absolute); +} +function assertExistingOwnedDirectory(value, label, options = {}) { + const absolute = requireProvisioningPath(value, label); + assertNotSymlink(absolute, label); + const stat = fs.statSync(absolute); + if (!stat.isDirectory()) throw new Error(`${label} must be a directory`); + if (options.private !== false) assertOwner(stat, label, options.ownerUid); + else if (options.ownerUid !== undefined && stat.uid !== options.ownerUid) throw new Error(`${label} is not owned by the current owner`); + return fs.realpathSync(absolute); +} +function atomicWriteJson(filePath, data, options = {}) { + const parent = path.dirname(filePath); + assertExistingOwnedDirectory(parent, 'registry parent directory', options); + const temporary = path.join(parent, `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(8).toString('hex')}.tmp`); + try { + fs.writeFileSync(temporary, `${JSON.stringify(data, null, 2)}\n`, { mode: 0o600, flag: 'wx' }); + fs.chmodSync(temporary, 0o600); + fs.renameSync(temporary, filePath); + fs.chmodSync(filePath, 0o600); + } finally { + if (fs.existsSync(temporary)) fs.unlinkSync(temporary); + } +} +function requireProvisioningPolicies(raw) { + for (const key of ['tracker', 'worktreePolicy', 'acceptancePolicy', 'providerEgressPolicy', 'credentialReferences', 'learning']) { + if (!isObject(raw[key])) throw new Error(`repository ${key} is required`); + } + if (typeof raw.learningPublicationTarget !== 'string' || !raw.learningPublicationTarget.trim()) throw new Error('repository learningPublicationTarget is required'); +} +function preparedEntry(raw, options = {}) { + if (!isObject(raw)) throw new Error('repository entry must be an object'); + requireProvisioningPolicies(raw); + const root = assertExistingOwnedDirectory(raw.root || raw.repositoryRoot, 'repository root', { ...options, private: false }); + const stateRoot = ensureOwnedDirectory(raw.stateRoot, 'repository stateRoot', options); + const worktreeRoot = ensureOwnedDirectory(raw.worktreePolicy.root || raw.worktreePolicy.worktreeRoot, 'repository worktreePolicy.root', options); + if (inside(root, stateRoot) || inside(root, worktreeRoot) || inside(stateRoot, root) || inside(worktreeRoot, root) || inside(stateRoot, worktreeRoot) || inside(worktreeRoot, stateRoot)) throw new Error('repository roots must not overlap'); + return normalizeEntry({ ...raw, root, stateRoot, worktreePolicy: { ...raw.worktreePolicy, root: worktreeRoot } }); +} +function registryTarget(registryPath, options = {}) { + const target = requireProvisioningPath(registryPath, 'registryPath'); + assertNotSymlink(target, 'registryPath', false); + assertExistingOwnedDirectory(path.dirname(target), 'registry parent directory', options); + if (fs.existsSync(target)) assertOwner(fs.statSync(target), 'registryPath', options.ownerUid); + return target; +} +function readProvisionedRegistry(registryPath, options = {}) { + const target = registryTarget(registryPath, options); + if (!fs.existsSync(target)) return { schemaVersion: REPOSITORY_REGISTRY_SCHEMA_VERSION, generation: 0, repositories: [] }; + const loaded = loadRepositoryRegistry(target, options); + return { schemaVersion: loaded.schemaVersion, generation: loaded.generation, repositories: loaded.repositories }; +} +function receiptFor(registry, action, repository) { + return Object.freeze({ schemaVersion: 'jarvos-coding-repository-provisioning-receipt/v1', action, generation: registry.generation, repository: repository ? publicRepository(repository) : null, repositoryCount: registry.repositories.length }); +} +function writeProvisionedRegistry(registryPath, registry, action, repository, options = {}) { + const target = registryTarget(registryPath, options); + const validation = validateRepositoryRegistry(registry); + if (!validation.ok) throw new Error(`invalid repository registry: ${validation.errors.join('; ')}`); + atomicWriteJson(target, registry, options); + const receipt = receiptFor(registry, action, repository); + atomicWriteJson(`${target}.receipt.json`, receipt, options); + return receipt; +} +function provisionRepository({ registryPath, repository, ownerUid } = {}) { + const existing = readProvisionedRegistry(registryPath, { ownerUid }); + const entry = preparedEntry(repository, { ownerUid }); + if (existing.repositories.some((item) => item.repositoryId === entry.repositoryId)) throw new Error('repositoryId is already provisioned; use update'); + return writeProvisionedRegistry(registryPath, { schemaVersion: REPOSITORY_REGISTRY_SCHEMA_VERSION, generation: existing.generation + 1, repositories: [...existing.repositories, entry] }, 'added', entry, { ownerUid }); +} +function inspectProvisionedRepositories({ registryPath, ownerUid } = {}) { + const existing = readProvisionedRegistry(registryPath, { ownerUid }); + return Object.freeze({ schemaVersion: 'jarvos-coding-repository-provisioning-inspection/v1', generation: existing.generation, repositories: existing.repositories.map(publicRepository) }); +} +function updateProvisionedRepository({ registryPath, repositoryId, repository, ownerUid } = {}) { + const existing = readProvisionedRegistry(registryPath, { ownerUid }); + const current = existing.repositories.find((item) => item.repositoryId === repositoryId); + if (!current) throw new Error('unknown repository'); + const entry = preparedEntry({ ...repository, repositoryId: current.repositoryId }, { ownerUid }); + const repositories = existing.repositories.map((item) => item.repositoryId === repositoryId ? entry : item); + return writeProvisionedRegistry(registryPath, { schemaVersion: REPOSITORY_REGISTRY_SCHEMA_VERSION, generation: existing.generation + 1, repositories }, 'updated', entry, { ownerUid }); +} +function revokeProvisionedRepository({ registryPath, repositoryId, ownerUid } = {}) { + const existing = readProvisionedRegistry(registryPath, { ownerUid }); + const current = existing.repositories.find((item) => item.repositoryId === repositoryId); + if (!current) throw new Error('unknown repository'); + return writeProvisionedRegistry(registryPath, { schemaVersion: REPOSITORY_REGISTRY_SCHEMA_VERSION, generation: existing.generation + 1, repositories: existing.repositories.filter((item) => item.repositoryId !== repositoryId) }, 'revoked', current, { ownerUid }); +} +function recordOwnerAction({ registryPath, repositoryId, action, runId, revision, ownerUid, now = new Date() } = {}) { + if (!new Set(['accept-plan', 'decline-learning', 'reset-learning-retry']).has(action)) throw new Error('owner action is invalid'); + if (typeof runId !== 'string' || !/^[A-Za-z0-9._:-]{1,160}$/.test(runId)) throw new Error('runId is required and must be an opaque identifier'); + if (action === 'accept-plan' && (typeof revision !== 'string' || !revision.trim() || revision.length > 512)) throw new Error('revision is required for accept-plan'); + const loaded = loadRepositoryRegistry(registryTarget(registryPath, { ownerUid }), { ownerUid }); + const repository = loaded.resolve(repositoryId); + const actionsPath = path.join(repository.stateRoot, 'owner-actions.json'); + let actions = { schemaVersion: OWNER_ACTIONS_SCHEMA_VERSION, generation: loaded.generation, actions: [] }; + if (fs.existsSync(actionsPath)) { + assertOwner(fs.statSync(actionsPath), 'owner action record', ownerUid); + actions = JSON.parse(fs.readFileSync(actionsPath, 'utf8')); + if (!isObject(actions) || actions.schemaVersion !== OWNER_ACTIONS_SCHEMA_VERSION || !Array.isArray(actions.actions)) throw new Error('owner action record is invalid'); + } + const record = { action, repositoryId: repository.repositoryId, runId, generation: loaded.generation, observedAt: now.toISOString() }; + if (revision) record.revision = revision; + atomicWriteJson(actionsPath, { schemaVersion: OWNER_ACTIONS_SCHEMA_VERSION, generation: loaded.generation, actions: [...actions.actions.filter((item) => !(item.action === action && item.runId === runId)), record] }, { ownerUid }); + return Object.freeze({ schemaVersion: 'jarvos-coding-owner-action-receipt/v1', action, repository: publicRepository(repository), runId, ...(revision ? { revision } : {}) }); +} + +module.exports = { REPOSITORY_REGISTRY_SCHEMA_VERSION, OWNER_ACTIONS_SCHEMA_VERSION, deriveRepositoryId, loadRepositoryRegistry, publicRepository, validateRepositoryRegistry, provisionRepository, inspectProvisionedRepositories, updateProvisionedRepository, revokeProvisionedRepository, recordOwnerAction }; diff --git a/modules/jarvos-coding/test/repository-provisioning.test.js b/modules/jarvos-coding/test/repository-provisioning.test.js new file mode 100644 index 00000000..bf7eb80d --- /dev/null +++ b/modules/jarvos-coding/test/repository-provisioning.test.js @@ -0,0 +1,81 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const test = require('node:test'); +const { + provisionRepository, + inspectProvisionedRepositories, + updateProvisionedRepository, + revokeProvisionedRepository, + recordOwnerAction, + loadRepositoryRegistry, +} = require('../src'); + +function fixture() { + const base = fs.mkdtempSync(path.join(os.tmpdir(), 'jarvos-provisioning-')); + const root = path.join(base, 'repository'); + fs.mkdirSync(root, 0o700); + return { + base, + registryPath: path.join(base, 'coding-registry.json'), + entry: { + publicLabel: 'Fixture repository', agentSelectable: true, root, + stateRoot: path.join(base, 'state'), + worktreePolicy: { root: path.join(base, 'worktrees') }, + tracker: { kind: 'fixture' }, acceptancePolicy: { mode: 'human-evidence-required' }, + providerEgressPolicy: { classes: ['plan'] }, credentialReferences: { tracker: 'keychain:fixture' }, + learning: { enabled: true }, learningPublicationTarget: 'vault:fixture', + }, + }; +} + +test('owner provisioning atomically adds, inspects, updates, and revokes a repository', () => { + const f = fixture(); + const added = provisionRepository({ registryPath: f.registryPath, repository: f.entry }); + assert.equal(added.generation, 1); + assert.equal(added.repository.label, 'Fixture repository'); + assert.equal(fs.statSync(f.registryPath).mode & 0o077, 0); + assert.equal(fs.statSync(f.entry.stateRoot).mode & 0o077, 0); + assert.equal(fs.statSync(f.entry.worktreePolicy.root).mode & 0o077, 0); + assert.ok(fs.existsSync(`${f.registryPath}.receipt.json`)); + assert.doesNotMatch(JSON.stringify(added), new RegExp(f.base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + + const inspected = inspectProvisionedRepositories({ registryPath: f.registryPath }); + assert.deepEqual(inspected.repositories, [{ repositoryId: added.repository.repositoryId, label: 'Fixture repository', agentSelectable: true }]); + + const updated = updateProvisionedRepository({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, repository: { ...f.entry, publicLabel: 'Renamed fixture', learning: { enabled: false } } }); + assert.equal(updated.generation, 2); + assert.equal(updated.repository.label, 'Renamed fixture'); + assert.equal(loadRepositoryRegistry(f.registryPath).generation, 2); + + const revoked = revokeProvisionedRepository({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId }); + assert.equal(revoked.generation, 3); + assert.deepEqual(inspectProvisionedRepositories({ registryPath: f.registryPath }).repositories, []); + assert.throws(() => loadRepositoryRegistry(f.registryPath).resolve(added.repository.repositoryId), /unknown repository/); +}); + +test('owner actions are scoped to one provisioned repository and plan revision', () => { + const f = fixture(); + const added = provisionRepository({ registryPath: f.registryPath, repository: f.entry }); + const accepted = recordOwnerAction({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, action: 'accept-plan', runId: 'run_1', revision: 'sha256:current' }); + assert.equal(accepted.action, 'accept-plan'); + assert.equal(accepted.revision, 'sha256:current'); + const declined = recordOwnerAction({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, action: 'decline-learning', runId: 'run_1' }); + assert.equal(declined.action, 'decline-learning'); + const reset = recordOwnerAction({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, action: 'reset-learning-retry', runId: 'run_1' }); + assert.equal(reset.action, 'reset-learning-retry'); + assert.throws(() => recordOwnerAction({ registryPath: f.registryPath, repositoryId: 'missing', action: 'decline-learning', runId: 'run_1' }), /unknown repository/); +}); + +test('provisioning fails closed for missing explicit authority and unsafe roots', () => { + const f = fixture(); + assert.throws(() => provisionRepository({ registryPath: f.registryPath, repository: { ...f.entry, acceptancePolicy: undefined } }), /acceptancePolicy is required/); + assert.throws(() => provisionRepository({ registryPath: f.registryPath, repository: { ...f.entry, learningPublicationTarget: undefined } }), /learningPublicationTarget is required/); + assert.throws(() => provisionRepository({ registryPath: f.registryPath, repository: { ...f.entry, stateRoot: f.entry.root } }), /must not overlap/); + const link = path.join(f.base, 'repository-link'); fs.symlinkSync(f.entry.root, link); + assert.throws(() => provisionRepository({ registryPath: f.registryPath, repository: { ...f.entry, root: link } }), /must not be a symbolic link/); + assert.throws(() => provisionRepository({ registryPath: undefined, repository: f.entry }), /registryPath is required/); +}); diff --git a/tests/cli-smoke-test.js b/tests/cli-smoke-test.js index 90b5cb04..aacd8fd8 100644 --- a/tests/cli-smoke-test.js +++ b/tests/cli-smoke-test.js @@ -280,6 +280,37 @@ try { assert.match(doctorBadHost.stdout, /configure a usable JARVOS_CONTROL_PLANE_SERVICE_MODULE/); assert.doesNotMatch(doctorBadHost.stdout, /missing-host\.js/); + const provisionedRoot = path.join(tmp, 'provisioned-repository'); + fs.mkdirSync(provisionedRoot, 0o700); + const registryPath = path.join(tmp, 'coding-registry.json'); + const repository = { + publicLabel: 'CLI fixture', agentSelectable: true, root: provisionedRoot, + stateRoot: path.join(tmp, 'coding-state'), worktreePolicy: { root: path.join(tmp, 'coding-worktrees') }, + tracker: { kind: 'fixture' }, acceptancePolicy: { mode: 'human-evidence-required' }, + providerEgressPolicy: {}, credentialReferences: { tracker: 'keychain:fixture' }, + learning: { enabled: true }, learningPublicationTarget: 'vault:fixture', + }; + const addedRepository = run(['coding', 'repository', 'add', '--registry', registryPath, '--repository-json', JSON.stringify(repository), '--json']); + assert.equal(addedRepository.status, 0, addedRepository.stderr || addedRepository.stdout); + const addReceipt = JSON.parse(addedRepository.stdout); + assert.equal(addReceipt.generation, 1); + assert.doesNotMatch(addedRepository.stdout, new RegExp(tmp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + const inspectedRepository = run(['coding', 'repository', 'inspect', '--registry', registryPath, '--json']); + assert.equal(inspectedRepository.status, 0, inspectedRepository.stderr || inspectedRepository.stdout); + assert.deepEqual(JSON.parse(inspectedRepository.stdout).repositories, [{ repositoryId: addReceipt.repository.repositoryId, label: 'CLI fixture', agentSelectable: true }]); + const acceptedPlan = run(['coding', 'accept', '--registry', registryPath, '--repository-id', addReceipt.repository.repositoryId, '--run-id', 'run_cli', '--revision', 'sha256:fixture', '--json']); + assert.equal(acceptedPlan.status, 0, acceptedPlan.stderr || acceptedPlan.stdout); + assert.equal(JSON.parse(acceptedPlan.stdout).revision, 'sha256:fixture'); + const declinedLearning = run(['coding', 'learning', 'decline', '--registry', registryPath, '--repository-id', addReceipt.repository.repositoryId, '--run-id', 'run_cli', '--json']); + assert.equal(declinedLearning.status, 0, declinedLearning.stderr || declinedLearning.stdout); + assert.equal(JSON.parse(declinedLearning.stdout).action, 'decline-learning'); + const resetLearning = run(['coding', 'learning', 'reset-retry', '--registry', registryPath, '--repository-id', addReceipt.repository.repositoryId, '--run-id', 'run_cli', '--json']); + assert.equal(resetLearning.status, 0, resetLearning.stderr || resetLearning.stdout); + assert.equal(JSON.parse(resetLearning.stdout).action, 'reset-learning-retry'); + const noRegistryInference = run(['coding', 'repository', 'inspect', '--json']); + assert.notEqual(noRegistryInference.status, 0); + assert.match(noRegistryInference.stderr, /registryPath is required/); + console.log('CLI smoke tests passed.'); } finally { fs.rmSync(tmp, { recursive: true, force: true }); From 4d00c12d62f00544abd0924634226b32c7134d23 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 14:54:48 -0400 Subject: [PATCH 03/12] feat(coding): add managed lifecycle controller --- modules/jarvos-coding/src/adapters/hosts.js | 5 +- .../src/adapters/native-workflow.js | 31 +++++ .../src/features/learning-eligibility.js | 5 + .../src/features/learning-signal.js | 30 +++++ .../src/features/work-run-store/index.js | 81 ++++++++++++ .../src/features/workflow/index.js | 120 +++++++++++++++--- modules/jarvos-coding/src/index.js | 4 + modules/jarvos-coding/src/runtime/codex.js | 17 ++- .../test/managed-workflow.test.js | 106 ++++++++++++++++ .../test/orchestrator-host-adapters.test.js | 8 ++ 10 files changed, 385 insertions(+), 22 deletions(-) create mode 100644 modules/jarvos-coding/src/adapters/native-workflow.js create mode 100644 modules/jarvos-coding/src/features/learning-eligibility.js create mode 100644 modules/jarvos-coding/src/features/learning-signal.js diff --git a/modules/jarvos-coding/src/adapters/hosts.js b/modules/jarvos-coding/src/adapters/hosts.js index 5fceb374..673bd00e 100644 --- a/modules/jarvos-coding/src/adapters/hosts.js +++ b/modules/jarvos-coding/src/adapters/hosts.js @@ -222,8 +222,9 @@ function createCodingHostAdapter(host, options = {}) { const adapters = await resolveAdapters(input); const managedWorkflow = adapters.managedWorkflow || options.managedWorkflow || null; const operation = input.operation || input.workflowOperation; - if (managedWorkflow && ['plan', 'work', 'complete', 'compound'].includes(operation)) { - const handler = managedWorkflow[operation]; + if (managedWorkflow && ['plan', 'accept-plan', 'acceptPlan', 'work', 'finish', 'status', 'resume'].includes(operation)) { + const normalizedOperation = operation === 'accept-plan' ? 'acceptPlan' : operation; + const handler = managedWorkflow[normalizedOperation]; if (typeof handler !== 'function') throw new Error(`managed coding workflow does not support ${operation}`); const result = await handler(input, adapters); return { diff --git a/modules/jarvos-coding/src/adapters/native-workflow.js b/modules/jarvos-coding/src/adapters/native-workflow.js new file mode 100644 index 00000000..d075082e --- /dev/null +++ b/modules/jarvos-coding/src/adapters/native-workflow.js @@ -0,0 +1,31 @@ +'use strict'; + +const { runTakeIssueToDone } = require('../features/orchestrator'); + +const NATIVE_WORKFLOW_SCHEMA_VERSION = 'jarvos-native-workflow/v1'; + +function createNativeWorkflowAdapter(options = {}) { + const execute = options.runTakeIssueToDone || runTakeIssueToDone; + if (typeof execute !== 'function') throw new Error('public native workflow executor is required'); + return Object.freeze({ + schemaVersion: NATIVE_WORKFLOW_SCHEMA_VERSION, + async plan(invocation) { + if (typeof options.plan !== 'function') throw new Error('public native planning dependency is unavailable'); + return options.plan(invocation); + }, + async work(invocation) { + if (typeof options.work === 'function') return options.work(invocation); + return execute({ ...invocation.input, workRunId: invocation.workRunId, canonicalWorktree: invocation.canonicalWorktree }, options.adapters || {}); + }, + async reconcileWork(invocation) { + if (typeof options.reconcileWork !== 'function') return { safe: false, reasonCode: 'authoritative_reconciliation_unavailable' }; + return options.reconcileWork(invocation); + }, + async verify(invocation) { + if (typeof options.verify !== 'function') throw new Error('public authoritative verification dependency is unavailable'); + return options.verify(invocation); + }, + }); +} + +module.exports = { NATIVE_WORKFLOW_SCHEMA_VERSION, createNativeWorkflowAdapter }; diff --git a/modules/jarvos-coding/src/features/learning-eligibility.js b/modules/jarvos-coding/src/features/learning-eligibility.js new file mode 100644 index 00000000..f09af40b --- /dev/null +++ b/modules/jarvos-coding/src/features/learning-eligibility.js @@ -0,0 +1,5 @@ +'use strict'; + +// Public feature path retained for consumers while the original provider +// contract remains source-compatible. +module.exports = require('../providers/learning-eligibility'); diff --git a/modules/jarvos-coding/src/features/learning-signal.js b/modules/jarvos-coding/src/features/learning-signal.js new file mode 100644 index 00000000..11198662 --- /dev/null +++ b/modules/jarvos-coding/src/features/learning-signal.js @@ -0,0 +1,30 @@ +'use strict'; + +const crypto = require('node:crypto'); +const { normalizeSignal, verifiedCodingOutcome } = require('../providers/learning-eligibility'); + +// Signals are deliberately derived from the authoritative completion object, +// never accepted from a tool caller. The orchestrator may attach a bounded +// candidate after its own submission gate has passed. +function deriveLearningSignal(verification = {}) { + if (!verifiedCodingOutcome(verification)) { + return { ok: false, status: 'not-eligible', reasonCode: 'coding_outcome_not_verified' }; + } + const candidate = verification.learning?.learning || verification.learningSignal + || (verification.nonRoutine === true || verification.submissionGate?.nonRoutine === true ? { + category: 'operational-lesson', + summary: 'Authoritative submission evidence must remain attached to verified non-routine coding work.', + } : null); + if (!candidate) return { ok: false, status: 'not-eligible', reasonCode: 'no_reusable_learning_signal' }; + const normalized = normalizeSignal(candidate); + if (!normalized.ok) return { ok: false, status: 'unsafe', reasonCode: 'unsafe_learning_signal', errors: normalized.errors }; + return { + ok: true, + signal: { + ...normalized.signal, + evidenceDigest: normalized.signal.evidenceDigest || crypto.createHash('sha256').update(JSON.stringify(verification.submissionGate || verification.events)).digest('hex'), + }, + }; +} + +module.exports = { deriveLearningSignal }; diff --git a/modules/jarvos-coding/src/features/work-run-store/index.js b/modules/jarvos-coding/src/features/work-run-store/index.js index 07dff60b..770e2a3a 100644 --- a/modules/jarvos-coding/src/features/work-run-store/index.js +++ b/modules/jarvos-coding/src/features/work-run-store/index.js @@ -147,6 +147,22 @@ function publicEvent(event) { } function publicRun(run) { + const learningTail = run.learningTail ? { + status: run.learningTail.status, + attempts: run.learningTail.attempts, + signal: run.learningTail.signal ? { + category: run.learningTail.signal.category, + summary: run.learningTail.signal.summary, + evidenceDigest: run.learningTail.signal.evidenceDigest, + } : null, + reasonCode: run.learningTail.reasonCode || null, + artifact: run.learningTail.artifact ? { + kind: run.learningTail.artifact.kind, + reference: run.learningTail.artifact.reference, + digest: run.learningTail.artifact.digest, + } : null, + updatedAt: run.learningTail.updatedAt, + } : null; return { version: WORK_RUN_PUBLIC_VERSION, workRunId: run.workRunId, @@ -159,6 +175,7 @@ function publicRun(run) { packetDigest: run.acceptedPlan.packetDigest || null, providerPinDigest: run.acceptedPlan.providerPinDigest, acceptedAt: run.acceptedPlan.acceptedAt, + acceptanceEvidence: run.acceptedPlan.acceptanceEvidence ? { source: run.acceptedPlan.acceptanceEvidence.source, observedAt: run.acceptedPlan.acceptanceEvidence.observedAt || null } : null, } : null, providerSnapshot: run.providerSnapshot ? { id: run.providerSnapshot.id, @@ -173,6 +190,7 @@ function publicRun(run) { events: run.events.map(publicEvent), recovery: clone(run.recovery), terminalEvidence: run.terminalEvidence ? clone(run.terminalEvidence) : null, + learningTail, createdAt: run.createdAt, updatedAt: run.updatedAt, }; @@ -233,6 +251,7 @@ function createWorkRunStore(options = {}) { eventNonces: {}, recovery: { state: 'active', reasonCode: null, updatedAt: now }, terminalEvidence: null, + learningTail: { status: 'not-evaluated', attempts: 0, signal: null, reasonCode: null, artifact: null, updatedAt: now }, createdAt: now, updatedAt: now, }; @@ -369,6 +388,7 @@ function createWorkRunStore(options = {}) { packetDigest: input.packetDigest || null, providerPinDigest: input.providerPinDigest || run.providerSnapshot?.pinDigest || null, acceptedAt: nowIso(clock), + acceptanceEvidence: input.acceptanceEvidence ? { source: input.acceptanceEvidence.source, observedAt: input.acceptanceEvidence.observedAt || null, planDigest: input.acceptanceEvidence.planDigest } : null, }; if (!run.artifacts.some((entry) => entry.reference === artifact.reference)) run.artifacts.push(artifact); run.updatedAt = run.acceptedPlan.acceptedAt; @@ -414,6 +434,64 @@ function createWorkRunStore(options = {}) { }); } + function setLearningTail(input = {}) { + return mutate((state) => { + const run = state.workRuns[input.workRunId]; + if (!run) return noCommit({ ok: false, reason: 'not_found' }); + const owner = assertRunOwner(run, input.ownerId, input.fence); + if (!owner.ok) return noCommit(owner); + const current = run.learningTail || { status: 'not-evaluated', attempts: 0, signal: null, artifact: null }; + const terminal = new Set(['captured', 'not-eligible', 'declined', 'unsafe', 'unavailable']); + if (terminal.has(current.status) && current.status !== input.status) return noCommit({ ok: false, reason: 'learning_tail_terminal', learningTail: clone(current) }); + if (!['not-evaluated', 'eligible', 'finalizing', 'captured', 'not-eligible', 'declined', 'unsafe', 'retryable-unavailable', 'failed', 'unavailable'].includes(input.status)) return noCommit({ ok: false, reason: 'invalid_learning_tail_status' }); + const signal = input.signal === undefined ? current.signal : input.signal; + if (signal !== null) assertSafeValue(signal, 'learningTail.signal'); + const next = { status: input.status, attempts: input.attempts === undefined ? current.attempts : input.attempts, signal: clone(signal), reasonCode: input.reasonCode || null, artifact: input.artifact || current.artifact || null, updatedAt: nowIso(clock) }; + run.learningTail = next; + run.updatedAt = next.updatedAt; + return { ok: true, learningTail: clone(next), workRun: clone(run), public: publicRun(run) }; + }); + } + + function reserveLearningFinalizer(input = {}) { + return mutate((state) => { + const run = state.workRuns[input.workRunId]; + if (!run) return noCommit({ ok: false, reason: 'not_found' }); + const owner = assertRunOwner(run, input.ownerId, input.fence); + if (!owner.ok) return noCommit(owner); + const current = run.learningTail || { status: 'not-evaluated', attempts: 0, signal: null }; + if (current.status === 'captured') return noCommit({ ok: true, deduped: true, learningTail: clone(current) }); + if (current.status === 'finalizing') return noCommit({ ok: false, reason: 'learning_finalizer_in_progress', learningTail: clone(current) }); + if (!current.signal) return noCommit({ ok: false, reason: 'learning_signal_missing', learningTail: clone(current) }); + if (current.attempts >= 3) { + current.status = 'unavailable'; current.reasonCode = 'learning_retry_budget_exhausted'; current.updatedAt = nowIso(clock); run.learningTail = current; run.updatedAt = current.updatedAt; + return { ok: false, reason: 'learning_retry_budget_exhausted', learningTail: clone(current) }; + } + const next = { ...current, status: 'finalizing', attempts: current.attempts + 1, updatedAt: nowIso(clock) }; + run.learningTail = next; run.updatedAt = next.updatedAt; + return { ok: true, deduped: false, learningTail: clone(next) }; + }); + } + + function reconcileLearningFinalizer(input = {}) { + return mutate((state) => { + const run = state.workRuns[input.workRunId]; + if (!run) return noCommit({ ok: false, reason: 'not_found' }); + const owner = assertRunOwner(run, input.ownerId, input.fence); + if (!owner.ok) return noCommit(owner); + const tail = run.learningTail; + if (!tail || tail.status !== 'finalizing') return noCommit({ ok: true, learningTail: clone(tail) }); + // A later status/resume is an explicit reconciliation boundary. The + // provider invocation remains idempotency-keyed; this only makes a + // crash-lost reservation retryable, it never creates a new artifact id. + tail.status = 'retryable-unavailable'; + tail.reasonCode = 'finalizer_reconciliation_required'; + tail.updatedAt = nowIso(clock); + run.updatedAt = tail.updatedAt; + return { ok: true, learningTail: clone(tail) }; + }); + } + function recordProviderReceipt(input = {}) { const validation = validateWorkflowProviderReceipt(input.receipt, { manifest: input.manifest, request: input.request }); if (!validation.ok) return { ok: false, reason: 'invalid_provider_receipt', errors: validation.errors }; @@ -443,6 +521,9 @@ function createWorkRunStore(options = {}) { acceptPlan, setRecoveryState, setTerminalEvidence, + setLearningTail, + reserveLearningFinalizer, + reconcileLearningFinalizer, projectPublicWorkRun: (workRun) => publicRun(workRun), validateState, }; diff --git a/modules/jarvos-coding/src/features/workflow/index.js b/modules/jarvos-coding/src/features/workflow/index.js index 930e4b0d..893451dc 100644 --- a/modules/jarvos-coding/src/features/workflow/index.js +++ b/modules/jarvos-coding/src/features/workflow/index.js @@ -16,6 +16,8 @@ const { const { runTakeIssueToDone, } = require('../orchestrator'); +const { deriveLearningSignal } = require('../learning-signal'); +const { assessTerminalSubmission } = require('../../adapters/hosts'); const MANAGED_WORKFLOW_SCHEMA_VERSION = 'jarvos-managed-coding-workflow/v1'; const IMPLEMENTATION_PACKET_VERSION = 'jarvos-implementation-packet/v1'; @@ -407,6 +409,13 @@ function createManagedCodingWorkflow(options = {}) { if (snapshot && !isTrustedProviderSnapshot(snapshot)) return { ok: false, status: 'blocked', workRunId: claimed.workRunId, reasonCode: 'provider_snapshot_untrusted' }; const packetValidation = validateImplementationPacket(input.packet, input.planDigest); if (!packetValidation.ok) return { ok: false, status: 'blocked', workRunId: claimed.workRunId, reasonCode: 'invalid_implementation_packet', errors: packetValidation.errors }; + // The standalone legacy constructor remains compatible; the public + // runtime always passes the owner-controlled policy explicitly. + const policy = input.acceptancePolicy || options.acceptancePolicy || { mode: 'agent-mediated-allowed' }; + const evidence = input.acceptanceEvidence || null; + if (policy.mode !== 'agent-mediated-allowed' && (!evidence || evidence.planDigest !== input.planDigest || typeof evidence.source !== 'string')) { + return { ok: false, status: 'awaiting-plan-acceptance', workRunId: claimed.workRunId, reasonCode: 'acceptance_evidence_required' }; + } return options.workRunStore.acceptPlan({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, @@ -416,6 +425,7 @@ function createManagedCodingWorkflow(options = {}) { providerPinDigest: snapshot?.pinDigest || null, packetDigest: digest(packetValidation.packet), artifact: input.artifact, + acceptanceEvidence: evidence, }); } @@ -551,7 +561,10 @@ function createManagedCodingWorkflow(options = {}) { type: 'route', operation: 'compound', status: outcome, - operationNonce: nonce, + // Keep the provider's stable operation nonce separate from route + // receipts. A retry must reuse the provider idempotency identity while + // still allowing the ledger to record a new route outcome. + operationNonce: `learning-outcome-${nonce}-${outcome}`, reasonCode: detail?.reasonCode || null, detail: detail?.rationale ? [detail.rationale] : null, }); @@ -589,12 +602,16 @@ function createManagedCodingWorkflow(options = {}) { }; } - const eligibility = evaluateLearningEligibility({ - verification: input.verification || input.orchestration, - signals: input.learningSignals ?? input.learning, - declined: input.declineLearning === true || input.declinedLearning === true, - }); - const nonce = learningNonce(claimed); + const eligibility = input.persistLearningSignal === true + ? { status: 'eligible', learning: input.learning, deferredCount: 0 } + : evaluateLearningEligibility({ + verification: input.verification || input.orchestration, + signals: input.learningSignals ?? input.learning, + declined: input.declineLearning === true || input.declinedLearning === true, + }); + const nonce = input.persistLearningSignal + ? `compound-${claimed.workRunId}` + : learningNonce(claimed); if (eligibility.status !== 'eligible') { return learningOutcomeResponse({ claimed, @@ -607,10 +624,17 @@ function createManagedCodingWorkflow(options = {}) { }); } + const persistedSignal = input.persistLearningSignal === true; + if (persistedSignal) { + const reserved = options.workRunStore.reserveLearningFinalizer({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence }); + if (!reserved.ok) return { ok: reserved.deduped === true, status: reserved.learningTail?.status || 'finalizing', learningStatus: reserved.learningTail?.status || 'finalizing', workRunId: claimed.workRunId, reasonCode: reserved.reason, route: 'jarvos-learning-gate' }; + } + const run = options.workRunStore.getWorkRun(claimed.workRunId, { public: false }); const acceptedPlanDigest = input.planDigest || run?.acceptedPlan?.digest; if (!acceptedPlanDigest) { const outcome = { status: 'unavailable', reasonCode: 'accepted_plan_missing', rationale: 'learning capture requires an accepted provider-independent plan revision' }; + if (persistedSignal) options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'retryable-unavailable', reasonCode: outcome.reasonCode }); return learningOutcomeResponse({ claimed, nonce, ...outcome, ok: true }); } @@ -619,12 +643,14 @@ function createManagedCodingWorkflow(options = {}) { request = requestFor('compound', { ...input, operationNonce: nonce }, claimed, acceptedPlanDigest); } catch (error) { const outcome = { status: 'unavailable', reasonCode: 'provider_identity_unavailable', rationale: 'approved provider identity is not available for learning capture' }; + if (persistedSignal) options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'retryable-unavailable', reasonCode: outcome.reasonCode }); return learningOutcomeResponse({ claimed, nonce, ...outcome, ok: true }); } const snapshot = providerSnapshotFor(input, claimed); if (!providerHealthy(manifest, snapshot, 'compound') || typeof providerAdapter.compound !== 'function') { const outcome = { status: 'unavailable', reasonCode: 'provider_unsupported', rationale: 'provider is not healthy for the active harness' }; + if (persistedSignal) options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'retryable-unavailable', reasonCode: outcome.reasonCode }); return learningOutcomeResponse({ claimed, nonce, ...outcome, ok: true }); } @@ -639,16 +665,19 @@ function createManagedCodingWorkflow(options = {}) { receipt = await invokeProvider('compound', invocation, providerAdapter.compound, input); } catch (error) { const outcome = { status: 'unavailable', reasonCode: 'provider_unavailable', rationale: 'provider did not return a learning receipt' }; + if (persistedSignal) options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'retryable-unavailable', reasonCode: outcome.reasonCode }); return learningOutcomeResponse({ claimed, nonce, ...outcome, ok: true }); } const validation = validateWorkflowProviderReceipt(receipt, { manifest, request }); if (!validation.ok) { const outcome = { status: 'failed', reasonCode: 'invalid_provider_receipt', rationale: 'provider learning receipt failed the strict contract' }; + if (persistedSignal) options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'failed', reasonCode: outcome.reasonCode }); return learningOutcomeResponse({ claimed, nonce, ...outcome, ok: false, errors: validation.errors }); } const screen = screenLearningReceipt(validation.receipt); if (!screen.ok) { const outcome = { status: 'failed', reasonCode: 'unsafe_learning_artifact', rationale: 'learning receipt contained private or unsafe content' }; + if (persistedSignal) options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'unsafe', reasonCode: outcome.reasonCode }); return learningOutcomeResponse({ claimed, nonce, ...outcome, ok: false, errors: screen.errors }); } const recorded = options.workRunStore.recordProviderReceipt({ @@ -663,8 +692,12 @@ function createManagedCodingWorkflow(options = {}) { const recording = providerReceiptRecording(recorded, validation.receipt); if (!recording.ok) { const outcome = { status: 'failed', reasonCode: recording.reason || 'learning_receipt_not_recorded', rationale: 'learning receipt could not be durably attached to the work run' }; + if (persistedSignal) options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'failed', reasonCode: outcome.reasonCode }); return { ok: false, ...outcome, learningStatus: outcome.status, route: 'jarvos-learning-gate', workRunId: claimed.workRunId, event: null }; } + if (persistedSignal && validation.receipt.status !== 'succeeded') { + options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'failed', reasonCode: 'provider_learning_failed' }); + } return { ok: validation.receipt.status === 'succeeded', status: validation.receipt.status, @@ -679,21 +712,69 @@ function createManagedCodingWorkflow(options = {}) { async function complete(input = {}, adapters = {}) { const claimed = claim(input); - const result = await runTakeIssueToDone({ ...input, workRunId: claimed.workRunId, branch: input.branch || input.branchName }, adapters); - if (result.learning?.status === 'eligible') { - const learning = await compound({ - ...input, - workRunId: claimed.workRunId, - learning: result.learning.learning, - learningEligibility: result.learning, - verification: result, - planDigest: input.planDigest || claimed.workRun.acceptedPlan?.digest, - }); - return { ...result, learning, workRunId: claimed.workRunId, route: 'jarvos-orchestrator' }; + if (!claimed.workRun.acceptedPlan || claimed.workRun.acceptedPlan.digest !== input.planDigest) { + return { ok: false, status: 'awaiting-plan-acceptance', workRunId: claimed.workRunId, reasonCode: 'accepted_plan_mismatch' }; } + const result = await runTakeIssueToDone({ ...input, workRunId: claimed.workRunId, branch: input.branch || input.branchName }, adapters); return { ...result, workRunId: claimed.workRunId, route: 'jarvos-orchestrator' }; } + async function finish(input = {}, adapters = {}) { + const claimed = claim(input); + const run = claimed.workRun; + if (!run.acceptedPlan || run.acceptedPlan.digest !== input.planDigest) { + return { ok: false, status: 'awaiting-plan-acceptance', workRunId: claimed.workRunId, reasonCode: 'accepted_plan_mismatch' }; + } + const result = await complete(input, adapters); + if (result.ok === false) return result; + const assessment = assessTerminalSubmission(result); + const verification = { + ...result, + submissionGate: assessment.submissionGate, + submissionEvidence: assessment.submissionEvidence, + nonRoutine: input.nonRoutine === true && input.routine !== true, + }; + if (result.status !== 'completed' || !assessment.ok) { + return { + ...result, + verification: null, + primaryCompletion: result.status, + learning: { + status: 'not-eligible', + reasonCode: result.status !== 'completed' ? 'coding_outcome_not_verified' : 'submission_gate_not_ready', + reasons: assessment.reasons, + }, + }; + } + const evidence = { + reference: `terminal_${digest(JSON.stringify(verification.submissionGate || verification.events || verification)).slice(0, 24)}`, + digest: digest(JSON.stringify(verification.submissionGate || verification.events || verification)), + status: result.status, + }; + const terminal = options.workRunStore.setTerminalEvidence({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, evidence }); + const derived = deriveLearningSignal(verification); + if (!derived.ok) { + options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: derived.status === 'unsafe' ? 'unsafe' : 'not-eligible', reasonCode: derived.reasonCode }); + return { ...result, verification: terminal.ok ? terminal.terminalEvidence : null, primaryCompletion: result.status, learning: { status: derived.status, reasonCode: derived.reasonCode } }; + } + options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'eligible', signal: derived.signal, reasonCode: 'reusable_learning_signal' }); + const learning = await compound({ ...input, workRunId: claimed.workRunId, verification, learning: derived.signal, persistLearningSignal: true }); + if (learning.learningStatus === 'captured' || learning.status === 'succeeded') options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'captured', artifact: learning.artifact || null }); + return { ...result, verification: terminal.ok ? terminal.terminalEvidence : null, primaryCompletion: result.status, learning }; + } + + async function catchUp(input = {}) { + const claimed = claim(input); + const run = options.workRunStore.getWorkRun(claimed.workRunId, { public: false }); + if (!run?.terminalEvidence) return { ok: true, status: run?.state || 'active', workRunId: claimed.workRunId, learning: null }; + const tail = run.learningTail || { status: 'not-evaluated' }; + if (!tail.signal || ['captured', 'not-eligible', 'declined', 'unsafe', 'unavailable'].includes(tail.status)) return { ok: true, status: 'verified', workRunId: claimed.workRunId, primaryCompletion: 'completed', learning: tail }; + if (tail.status === 'finalizing') options.workRunStore.reconcileLearningFinalizer({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence }); + const learning = await compound({ ...input, workRunId: claimed.workRunId, planDigest: run.acceptedPlan?.digest, verification: { status: 'completed', submissionGate: { ready: true }, events: [] }, learning: tail.signal, persistLearningSignal: true }); + if (learning.learningStatus === 'captured' || learning.status === 'succeeded') options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'captured', artifact: learning.artifact || null }); + return { ok: true, status: 'verified', workRunId: claimed.workRunId, primaryCompletion: 'completed', learning }; + } + return { manifest, plan, @@ -701,6 +782,9 @@ function createManagedCodingWorkflow(options = {}) { work, compound, complete, + finish, + status: catchUp, + resume: catchUp, validateImplementationPacket, buildProviderInvocation, providerHealthy: (snapshot, operation) => providerHealthy(manifest, snapshot, operation), diff --git a/modules/jarvos-coding/src/index.js b/modules/jarvos-coding/src/index.js index 046fb0b6..1839fe8c 100644 --- a/modules/jarvos-coding/src/index.js +++ b/modules/jarvos-coding/src/index.js @@ -160,6 +160,8 @@ const projectsActivity = require('./projects-activity'); const compoundEngineeringProvider = require('./providers/compound-engineering'); const workflowProvider = require('./providers/workflow-provider'); const learningEligibility = require('./providers/learning-eligibility'); +const learningSignal = require('./features/learning-signal'); +const nativeWorkflow = require('./adapters/native-workflow'); const workRunStore = require('./features/work-run-store'); const managedWorkflow = require('./features/workflow'); const codexRuntime = require('./runtime/codex'); @@ -173,6 +175,8 @@ module.exports = { ...compoundEngineeringProvider, ...workflowProvider, ...learningEligibility, + ...learningSignal, + ...nativeWorkflow, ...workRunStore, ...managedWorkflow, ...codexRuntime, diff --git a/modules/jarvos-coding/src/runtime/codex.js b/modules/jarvos-coding/src/runtime/codex.js index 2c2790eb..baa4fd0a 100644 --- a/modules/jarvos-coding/src/runtime/codex.js +++ b/modules/jarvos-coding/src/runtime/codex.js @@ -1,9 +1,12 @@ 'use strict'; const crypto = require('node:crypto'); +const path = require('node:path'); const { buildLiveCodingAdapters } = require('../adapters/live'); const { createFileWorkRunStore } = require('../features/work-run-store'); const { loadRepositoryRegistry } = require('./repository-registry'); +const { createManagedCodingWorkflow } = require('../features/workflow'); +const { createNativeWorkflowAdapter } = require('../adapters/native-workflow'); const CODEX_RUNTIME_SCHEMA_VERSION = 'jarvos-coding-codex-runtime/v1'; const SUBJECT = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/; @@ -35,9 +38,19 @@ function createCodexRuntime(options = {}) { const existing = store.getWorkRun(workRunId, { public: false }); if (existing && existing.subjectKey !== qualifiedSubject) throw new Error('workRunId belongs to a different repository-qualified subject'); if (existing?.canonicalWorktree && existing.canonicalWorktree !== worktree) throw new Error('work run canonical worktree no longer matches repository authority'); - return Object.freeze({ repository, repositoryId: repository.repositoryId, subjectKey: qualifiedSubject, workRunId, canonicalWorktree: worktree, store, + const liveAdapters = buildLiveCodingAdapters({ ...(options.liveAdapters || {}), repoRootDir: repository.root, worktreeRoot: repository.worktreePolicy.root, repo: repository.tracker.repo }); + const nativeAdapter = options.nativeAdapter || createNativeWorkflowAdapter({ ...(options.nativeWorkflow || {}), adapters: liveAdapters }); + const managedWorkflow = createManagedCodingWorkflow({ + ...(options.managedWorkflow || {}), + workRunStore: store, + nativeAdapter, + manifestPath: options.managedWorkflow?.manifestPath || path.resolve(__dirname, '../../providers/compound-engineering.json'), + acceptancePolicy: repository.acceptancePolicy, + ownerId: options.ownerId || 'jarvos-coding', + }); + return Object.freeze({ repository, repositoryId: repository.repositoryId, subjectKey: qualifiedSubject, workRunId, canonicalWorktree: worktree, store, managedWorkflow, public: Object.freeze({ version: CODEX_RUNTIME_SCHEMA_VERSION, repository: { repositoryId: repository.repositoryId, label: repository.publicLabel }, subjectKey: qualifiedSubject, workRunId }), - adapters: buildLiveCodingAdapters({ ...(options.liveAdapters || {}), repoRootDir: repository.root, worktreeRoot: repository.worktreePolicy.root, repo: repository.tracker.repo }), + adapters: liveAdapters, }); } return Object.freeze({ schemaVersion: CODEX_RUNTIME_SCHEMA_VERSION, resolveRequest, listRepositories: () => registry.listPublic(), health: () => ({ version: CODEX_RUNTIME_SCHEMA_VERSION, status: 'installed-but-unwired', registryGeneration: registry.generation, repositories: registry.listPublic() }) }); diff --git a/modules/jarvos-coding/test/managed-workflow.test.js b/modules/jarvos-coding/test/managed-workflow.test.js index 5e21eb93..69e02354 100644 --- a/modules/jarvos-coding/test/managed-workflow.test.js +++ b/modules/jarvos-coding/test/managed-workflow.test.js @@ -7,6 +7,7 @@ const { IMPLEMENTATION_PACKET_VERSION, createManagedCodingWorkflow, createMemoryWorkRunStore, + deriveLearningSignal, validateImplementationPacket, } = require('../src'); @@ -460,3 +461,108 @@ test('timed-out work waits for provider settlement, then reconciles once and rep assert.equal(replay.deduped, true); assert.equal(nativeWorkCalls, 1); }); + +test('controller acceptance requires owner evidence under the public human-evidence policy', async () => { + const currentManifest = manifest(); + const workflow = createManagedCodingWorkflow({ + manifest: currentManifest, + workRunStore: createMemoryWorkRunStore(), + ownerId: 'agent:codex', + acceptancePolicy: { mode: 'human-evidence-required' }, + }); + const input = { subjectKey: 'levineam/jarvOS:SUP-5013', canonicalWorktree: '/private/jarvos/worktrees/SUP-5013', planDigest: '3'.repeat(64), packet: packet('3'.repeat(64)), artifact: { reference: 'artifact:plan123456', digest: '3'.repeat(64) } }; + const blocked = await workflow.acceptPlan(input); + assert.equal(blocked.status, 'awaiting-plan-acceptance'); + const accepted = await workflow.acceptPlan({ ...input, acceptanceEvidence: { source: 'owner-cli', planDigest: input.planDigest } }); + assert.equal(accepted.ok, true); +}); + +test('legacy complete is fail-closed before accepted plan evidence and never trusts caller learning', async () => { + const workflow = createManagedCodingWorkflow({ manifest: manifest(), workRunStore: createMemoryWorkRunStore(), ownerId: 'agent:codex' }); + const result = await workflow.complete({ + subjectKey: 'levineam/jarvOS:SUP-5014', + canonicalWorktree: '/private/jarvos/worktrees/SUP-5014', + planDigest: '4'.repeat(64), + learning: { category: 'root-cause', summary: 'caller supplied learning must not publish' }, + }); + assert.equal(result.status, 'awaiting-plan-acceptance'); +}); + +test('durable learning-tail reservation survives a restart shape and allows only one finalizer attempt', () => { + const store = createMemoryWorkRunStore(); + const claim = store.claimWorkRun({ subjectKey: 'levineam/jarvOS:SUP-5016', canonicalWorktree: '/private/jarvos/worktrees/SUP-5016', ownerId: 'agent:codex' }); + store.setLearningTail({ workRunId: claim.workRunId, ownerId: claim.ownerId, fence: claim.fence, status: 'eligible', signal: { category: 'operational-lesson', summary: 'Preserve verified evidence', evidenceDigest: '6'.repeat(64) } }); + const first = store.reserveLearningFinalizer({ workRunId: claim.workRunId, ownerId: claim.ownerId, fence: claim.fence }); + const replay = store.reserveLearningFinalizer({ workRunId: claim.workRunId, ownerId: claim.ownerId, fence: claim.fence }); + assert.equal(first.ok, true); + assert.equal(replay.reason, 'learning_finalizer_in_progress'); + assert.equal(store.getWorkRun(claim.workRunId, { public: false }).learningTail.attempts, 1); +}); + +test('status reconciles an interrupted finalizer reservation into the same durable retry tail', async () => { + const store = createMemoryWorkRunStore(); + const workflow = createManagedCodingWorkflow({ manifest: manifest(), workRunStore: store, ownerId: 'agent:codex' }); + const subjectKey = 'levineam/jarvOS:SUP-5017'; + const claim = store.claimWorkRun({ subjectKey, canonicalWorktree: '/private/jarvos/worktrees/SUP-5017', ownerId: 'agent:codex' }); + store.acceptPlan({ workRunId: claim.workRunId, ownerId: claim.ownerId, fence: claim.fence, planDigest: '7'.repeat(64), packetDigest: '8'.repeat(64), artifact: { reference: 'artifact:plan123456', digest: '7'.repeat(64) } }); + store.setTerminalEvidence({ workRunId: claim.workRunId, ownerId: claim.ownerId, fence: claim.fence, evidence: { reference: 'terminal_123456', digest: '9'.repeat(64), status: 'completed' } }); + store.setLearningTail({ workRunId: claim.workRunId, ownerId: claim.ownerId, fence: claim.fence, status: 'eligible', signal: { category: 'operational-lesson', summary: 'Preserve verified evidence', evidenceDigest: 'a'.repeat(64) } }); + store.reserveLearningFinalizer({ workRunId: claim.workRunId, ownerId: claim.ownerId, fence: claim.fence }); + const resumed = await workflow.status({ subjectKey, canonicalWorktree: '/private/jarvos/worktrees/SUP-5017', planDigest: '7'.repeat(64) }); + assert.equal(resumed.status, 'verified'); + assert.equal(store.getWorkRun(claim.workRunId, { public: false }).learningTail.status, 'retryable-unavailable'); +}); + +test('persisted learning retries reuse one provider operation identity and artifact', async () => { + const currentManifest = manifest(); + const currentProvider = provider(currentManifest); + const store = createMemoryWorkRunStore(); + const invocations = []; + const workflow = createManagedCodingWorkflow({ + manifest: currentManifest, + workRunStore: store, + ownerId: 'agent:codex', + providerSnapshot: currentProvider, + providerAdapter: { + compound: async (invocation) => { + invocations.push(invocation); + return receipt(invocation, 'compound', 'b'.repeat(64)); + }, + }, + }); + const input = { + subjectKey: 'levineam/jarvOS:SUP-5018', + canonicalWorktree: '/private/jarvos/worktrees/SUP-5018', + planDigest: 'b'.repeat(64), + }; + const claim = store.claimWorkRun({ ...input, ownerId: 'agent:codex', providerSnapshot: currentProvider }); + store.acceptPlan({ + workRunId: claim.workRunId, + ownerId: claim.ownerId, + fence: claim.fence, + planDigest: input.planDigest, + packetDigest: 'c'.repeat(64), + providerPinDigest: currentProvider.pinDigest, + artifact: { reference: 'artifact:plan123456', digest: input.planDigest }, + }); + store.setLearningTail({ + workRunId: claim.workRunId, + ownerId: claim.ownerId, + fence: claim.fence, + status: 'eligible', + signal: { category: 'operational-lesson', summary: 'Preserve the verified operation identity', evidenceDigest: 'd'.repeat(64) }, + }); + const first = await workflow.compound({ ...input, learning: store.getWorkRun(claim.workRunId, { public: false }).learningTail.signal, persistLearningSignal: true }); + const second = await workflow.compound({ ...input, learning: store.getWorkRun(claim.workRunId, { public: false }).learningTail.signal, persistLearningSignal: true }); + assert.equal(first.learningStatus, 'captured'); + assert.equal(second.reasonCode, 'one_learning_per_work_run'); + assert.equal(invocations.length, 1); + assert.equal(invocations[0].idempotencyKey, `compound:${claim.workRunId}`); +}); + +test('a verified non-routine authoritative result derives a learning candidate without caller signals', () => { + const verification = { status: 'completed', submissionGate: { ready: true, nonRoutine: true }, events: ['claim', 'branch', 'sliceReview', 'holisticReview', 'fixRerun', 'pullRequest', 'postMergeSweep', 'verifyClose'].map((stage) => ({ stage, result: stage === 'verifyClose' ? { status: 'closed' } : { status: 'completed' } })) }; + const derived = deriveLearningSignal(verification); + assert.equal(derived.ok, true); + assert.equal(derived.signal.category, 'operational-lesson'); +}); diff --git a/modules/jarvos-coding/test/orchestrator-host-adapters.test.js b/modules/jarvos-coding/test/orchestrator-host-adapters.test.js index 56e98a36..db0dfd9c 100644 --- a/modules/jarvos-coding/test/orchestrator-host-adapters.test.js +++ b/modules/jarvos-coding/test/orchestrator-host-adapters.test.js @@ -841,3 +841,11 @@ test('Hermes host adapter registers the existing coding entrypoint once with poi /continuityReference may not include transcript/, ); }); + +test('host adapters expose controller finish and never route legacy complete', async () => { + const calls = []; + const adapter = createCodexHostAdapter({ adapters: { managedWorkflow: { async finish(input) { calls.push(input.subjectKey); return { ok: true, status: 'completed' }; } } } }); + const result = await adapter.runTakeIssueToDone({ operation: 'finish', subjectKey: 'levineam/jarvOS:SUP-5015' }); + assert.equal(result.operation, 'finish'); + assert.deepEqual(calls, ['levineam/jarvOS:SUP-5015']); +}); From 43a44ec7c5c879a719fed24560d957a4f3fb69d2 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 15:08:36 -0400 Subject: [PATCH 04/12] feat(coding): expose managed Codex workflow --- modules/jarvos-coding/package.json | 6 +- .../scripts/jarvos-coding-mcp.js | 98 +++++++++++++++++++ .../src/runtime/repository-registry.js | 25 ++++- .../test/coding-mcp-pack.test.js | 30 ++++++ modules/jarvos-coding/test/coding-mcp.test.js | 67 +++++++++++++ .../test/repository-provisioning.test.js | 11 +++ modules/jarvos-skills/manifest.json | 7 +- .../skills/workflow-execution/SKILL.md | 45 ++++++--- modules/jarvos-skills/test/projection.test.js | 20 +++- package.json | 1 + runtimes/codex/adapter.json | 13 ++- runtimes/codex/setup.sh | 48 +++++++++ tests/pack-manifest-test.js | 1 + 13 files changed, 349 insertions(+), 23 deletions(-) create mode 100755 modules/jarvos-coding/scripts/jarvos-coding-mcp.js create mode 100644 modules/jarvos-coding/test/coding-mcp-pack.test.js create mode 100644 modules/jarvos-coding/test/coding-mcp.test.js diff --git a/modules/jarvos-coding/package.json b/modules/jarvos-coding/package.json index b59f1570..b9d8ea64 100644 --- a/modules/jarvos-coding/package.json +++ b/modules/jarvos-coding/package.json @@ -4,10 +4,14 @@ "description": "Portable jarvOS coding orchestrator with thin Claude Code, Codex, and OpenClaw host adapters.", "license": "MIT", "main": "src/index.js", + "bin": { + "jarvos-coding-mcp": "scripts/jarvos-coding-mcp.js" + }, "files": [ "README.md", "src/", - "providers/" + "providers/", + "scripts/" ], "exports": { ".": "./src/index.js" diff --git a/modules/jarvos-coding/scripts/jarvos-coding-mcp.js b/modules/jarvos-coding/scripts/jarvos-coding-mcp.js new file mode 100755 index 00000000..48aa6ace --- /dev/null +++ b/modules/jarvos-coding/scripts/jarvos-coding-mcp.js @@ -0,0 +1,98 @@ +#!/usr/bin/env node +'use strict'; + +// Public, JSON-lines MCP boundary for managed coding. Host configuration is +// intentionally limited to a private registry binding; no filesystem roots, +// provider selection, executables, or credentials cross this protocol. +const readline = require('node:readline'); +// Import only the public runtime boundary, rather than the package barrel: +// MCP initialization must work from an unpacked tarball before any repository +// registry is configured. +const { createCodexRuntime } = require('../src/runtime/codex'); +const { resolveOwnerPlanAcceptance } = require('../src/runtime/repository-registry'); + +const REGISTRY_ENV = 'JARVOS_CODING_REPOSITORY_REGISTRY'; +const SHA256 = /^[a-f0-9]{64}$/i; +const OPAQUE = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; +const SUBJECT = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/; +const SAFE_TEXT = /^[^\0\r\n]{1,500}$/; +const PATHISH = /(?:^|[\s"'])\/(?:[^\s"']*)/; +const SECRET = /(?:\bBearer\s+|\bsk-[A-Za-z0-9_-]{8,}|\bxox[baprs]-|(?:api[_-]?key|token|secret|password)\s*[:=])/i; + +const TOOLS = [ + ['jarvos_coding_plan', 'Create or route a managed plan for an owner-provisioned repository subject.'], + ['jarvos_coding_accept_plan', 'Accept a plan only when matching durable owner acceptance already exists.'], + ['jarvos_coding_work', 'Run accepted managed work for an owner-provisioned repository subject.'], + ['jarvos_coding_finish', 'Run the managed completion gate for an accepted plan.'], + ['jarvos_coding_status', 'Return public managed-work status.'], + ['jarvos_coding_resume', 'Resume safe managed-work reconciliation.'], + ['jarvos_coding_repositories', 'List agent-selectable owner-provisioned repositories.'], + ['jarvos_coding_health', 'Return public coding runtime health.'], +].map(([name, description]) => ({ name, description, inputSchema: schemaFor(name) })); + +function schemaFor(name) { + const base = { + type: 'object', additionalProperties: false, + required: ['repositoryId', 'subjectKey'], + properties: { repositoryId: { type: 'string', pattern: OPAQUE.source }, subjectKey: { type: 'string', pattern: SUBJECT.source }, workRunId: { type: 'string', pattern: OPAQUE.source } }, + }; + if (name === 'jarvos_coding_repositories' || name === 'jarvos_coding_health') return { type: 'object', additionalProperties: false, properties: {} }; + if (name === 'jarvos_coding_plan') return { ...base, properties: { ...base.properties, input: boundedInputSchema(), operationNonce: { type: 'string', maxLength: 128 } } }; + if (name === 'jarvos_coding_accept_plan' || name === 'jarvos_coding_work') return { ...base, required: [...base.required, 'planDigest', 'packet'], properties: { ...base.properties, planDigest: digestSchema(), expectedPlanDigest: { anyOf: [digestSchema(), { type: 'null' }] }, packet: packetSchema(), artifact: artifactSchema(), operationNonce: { type: 'string', maxLength: 128 } } }; + if (name === 'jarvos_coding_finish') return { ...base, required: [...base.required, 'planDigest'], properties: { ...base.properties, planDigest: digestSchema() } }; + return base; +} +function digestSchema() { return { type: 'string', pattern: SHA256.source }; } +function boundedInputSchema() { return { type: 'object', additionalProperties: false, required: ['kind', 'digest'], properties: { kind: { type: 'string', pattern: '^[A-Za-z0-9._-]{1,80}$' }, digest: digestSchema() } }; } +function artifactSchema() { return { type: 'object', additionalProperties: false, required: ['reference'], properties: { reference: { type: 'string', pattern: '^artifact:[A-Za-z0-9._-]{6,160}$' } } }; } +function packetSchema() { return { type: 'object', additionalProperties: false, required: ['version', 'planDigest', 'steps'], properties: { version: { const: 'jarvos-implementation-packet/v1' }, planDigest: digestSchema(), summary: { type: 'string', maxLength: 500 }, steps: { type: 'array', minItems: 1, maxItems: 128, items: { type: 'object', additionalProperties: false, required: ['id', 'description'], properties: { id: { type: 'string', pattern: OPAQUE.source }, description: { type: 'string', maxLength: 500 }, files: { type: 'array', maxItems: 64, items: { type: 'string' } }, mutation: { type: 'string', maxLength: 500 } } } } } }; } + +function fail(message, code = -32602) { const error = new Error(message); error.code = code; throw error; } +function object(value, label) { if (!value || typeof value !== 'object' || Array.isArray(value)) fail(`${label} must be an object`); return value; } +function known(value, allowed, label) { for (const key of Object.keys(value)) if (!allowed.has(key)) fail(`${label}.${key} is not allowed`); } +function safeString(value, label, max = 500) { if (typeof value !== 'string' || !SAFE_TEXT.test(value) || value.length > max || PATHISH.test(value) || SECRET.test(value)) fail(`${label} is unsafe`); return value; } +function digest(value, label) { if (typeof value !== 'string' || !SHA256.test(value)) fail(`${label} must be a SHA-256 digest`); return value; } +function identity(args) { object(args, 'arguments'); known(args, new Set(['repositoryId', 'subjectKey', 'workRunId', 'input', 'operationNonce', 'planDigest', 'expectedPlanDigest', 'packet', 'artifact']), 'arguments'); if (!OPAQUE.test(args.repositoryId || '')) fail('repositoryId is required'); if (typeof args.subjectKey !== 'string' || !SUBJECT.test(args.subjectKey)) fail('subjectKey must be a safe stable identifier'); if (args.workRunId !== undefined && !OPAQUE.test(args.workRunId)) fail('workRunId must be opaque'); return { repositoryId: args.repositoryId, subjectKey: args.subjectKey, ...(args.workRunId ? { workRunId: args.workRunId } : {}) }; } +function packet(value, planDigest) { object(value, 'packet'); known(value, new Set(['version', 'planDigest', 'steps', 'summary']), 'packet'); if (value.version !== 'jarvos-implementation-packet/v1') fail('packet.version is invalid'); digest(value.planDigest, 'packet.planDigest'); if (value.planDigest !== planDigest) fail('packet.planDigest must match planDigest'); if (!Array.isArray(value.steps) || value.steps.length < 1 || value.steps.length > 128) fail('packet.steps must contain 1 to 128 steps'); for (const [i, step] of value.steps.entries()) { object(step, `packet.steps[${i}]`); known(step, new Set(['id', 'description', 'files', 'mutation']), `packet.steps[${i}]`); if (!OPAQUE.test(step.id || '')) fail(`packet.steps[${i}].id is invalid`); safeString(step.description, `packet.steps[${i}].description`); if (step.mutation !== undefined) safeString(step.mutation, `packet.steps[${i}].mutation`); if (step.files !== undefined && (!Array.isArray(step.files) || step.files.some((file) => typeof file !== 'string' || file.startsWith('/') || file.includes('..') || !/^[A-Za-z0-9._/-]+$/.test(file)))) fail(`packet.steps[${i}].files is invalid`); } if (value.summary !== undefined) safeString(value.summary, 'packet.summary'); return value; } +function publicValue(value) { if (value == null || typeof value === 'boolean' || typeof value === 'number') return value; if (typeof value === 'string') return (value.length <= 1000 && !PATHISH.test(value) && !SECRET.test(value)) ? value : '[redacted]'; if (Array.isArray(value)) return value.map(publicValue); if (typeof value === 'object') { const output = {}; for (const [key, entry] of Object.entries(value)) if (!/^(?:root|path|worktree|credential|provider|command|executable|detail)$/i.test(key)) output[key] = publicValue(entry); return output; } return null; } +function textResult(result, isError = false) { return { content: [{ type: 'text', text: JSON.stringify(publicValue(result)) }], isError }; } +function hostRuntime(options = {}) { const registryPath = options.registryPath || process.env[REGISTRY_ENV]; if (typeof registryPath !== 'string' || !registryPath) fail('coding MCP host registry binding is not configured', -32000); return (options.createRuntime || createCodexRuntime)({ registryPath, ...(options.runtimeOptions || {}) }); } + +async function callTool(name, args = {}, options = {}) { + if (name === 'jarvos_coding_repositories') { object(args, 'arguments'); known(args, new Set(), 'arguments'); return textResult({ ok: true, repositories: hostRuntime(options).listRepositories() }); } + if (name === 'jarvos_coding_health') { object(args, 'arguments'); known(args, new Set(), 'arguments'); return textResult({ ok: true, health: hostRuntime(options).health() }); } + const names = new Set(TOOLS.map((tool) => tool.name)); if (!names.has(name)) fail(`Unknown tool: ${name}`, -32601); + const allowedByTool = { + jarvos_coding_plan: new Set(['repositoryId', 'subjectKey', 'workRunId', 'input', 'operationNonce']), + jarvos_coding_accept_plan: new Set(['repositoryId', 'subjectKey', 'workRunId', 'planDigest', 'packet', 'expectedPlanDigest', 'artifact', 'operationNonce']), + jarvos_coding_work: new Set(['repositoryId', 'subjectKey', 'workRunId', 'planDigest', 'packet', 'operationNonce']), + jarvos_coding_finish: new Set(['repositoryId', 'subjectKey', 'workRunId', 'planDigest']), + jarvos_coding_status: new Set(['repositoryId', 'subjectKey', 'workRunId']), + jarvos_coding_resume: new Set(['repositoryId', 'subjectKey', 'workRunId']), + }; + known(object(args, 'arguments'), allowedByTool[name], 'arguments'); + const input = identity(args); const runtime = hostRuntime(options); const context = runtime.resolveRequest(input); const workflow = context.managedWorkflow; + // The runtime owns the qualified persistence subject. Native execution gets + // the original opaque tracker identifier as its bounded work reference. + const workflowInput = { ...input, subjectKey: context.subjectKey || input.subjectKey, issueIdentifier: args.subjectKey }; + if (name === 'jarvos_coding_plan') { if (args.input !== undefined) { object(args.input, 'input'); known(args.input, new Set(['kind', 'digest']), 'input'); safeString(args.input.kind, 'input.kind', 80); digest(args.input.digest, 'input.digest'); } if (args.operationNonce !== undefined) safeString(args.operationNonce, 'operationNonce', 128); return textResult(await workflow.plan({ ...workflowInput, input: args.input, operationNonce: args.operationNonce })); } + if (name === 'jarvos_coding_status' || name === 'jarvos_coding_resume') return textResult(await workflow[name === 'jarvos_coding_status' ? 'status' : 'resume'](workflowInput)); + const planDigest = digest(args.planDigest, 'planDigest'); + if (name === 'jarvos_coding_finish') return textResult(await workflow.finish({ ...workflowInput, planDigest })); + const implementationPacket = packet(args.packet, planDigest); if (args.operationNonce !== undefined) safeString(args.operationNonce, 'operationNonce', 128); + if (name === 'jarvos_coding_accept_plan') { + if (args.expectedPlanDigest !== undefined && args.expectedPlanDigest !== null) digest(args.expectedPlanDigest, 'expectedPlanDigest'); + if (args.artifact !== undefined) { object(args.artifact, 'artifact'); known(args.artifact, new Set(['reference']), 'artifact'); if (typeof args.artifact.reference !== 'string' || !/^artifact:[A-Za-z0-9._-]{6,160}$/.test(args.artifact.reference)) fail('artifact.reference is invalid'); } + let acceptanceEvidence = null; + if (context.repository.acceptancePolicy.mode !== 'agent-mediated-allowed') acceptanceEvidence = (options.resolveOwnerAcceptance || resolveOwnerPlanAcceptance)({ registryPath: options.registryPath || process.env[REGISTRY_ENV], repositoryId: context.repositoryId, runId: context.workRunId, planDigest, ...(options.ownerUid === undefined ? {} : { ownerUid: options.ownerUid }) }); + if (context.repository.acceptancePolicy.mode !== 'agent-mediated-allowed' && !acceptanceEvidence) return textResult({ ok: false, status: 'awaiting-plan-acceptance', workRunId: context.workRunId, reasonCode: 'owner_acceptance_required' }, true); + return textResult(await workflow.acceptPlan({ ...workflowInput, planDigest, packet: implementationPacket, expectedPlanDigest: args.expectedPlanDigest, artifact: args.artifact, operationNonce: args.operationNonce, acceptanceEvidence })); + } + return textResult(await workflow.work({ ...workflowInput, planDigest, packet: implementationPacket, operationNonce: args.operationNonce })); +} + +function write(message) { process.stdout.write(`${JSON.stringify(message)}\n`); } +async function handle(message, options = {}) { if (!message || typeof message !== 'object') return; const { id, method, params } = message; if (!id && String(method || '').startsWith('notifications/')) return; try { if (method === 'initialize') return write({ jsonrpc: '2.0', id, result: { protocolVersion: params?.protocolVersion || '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'jarvos-coding', version: '0.1.0' } } }); if (method === 'tools/list') return write({ jsonrpc: '2.0', id, result: { tools: TOOLS } }); if (method === 'tools/call') return write({ jsonrpc: '2.0', id, result: await callTool(params?.name, params?.arguments === undefined ? {} : params.arguments, options) }); write({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } }); } catch (error) { write({ jsonrpc: '2.0', id, error: { code: error.code || -32000, message: publicValue(error.message || String(error)) } }); } } +function main() { const rl = readline.createInterface({ input: process.stdin }); rl.on('line', (line) => { if (!line.trim()) return; try { handle(JSON.parse(line)); } catch (error) { write({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' } }); } }); } +if (require.main === module) main(); +module.exports = { TOOLS, callTool, handle, schemaFor, REGISTRY_ENV }; diff --git a/modules/jarvos-coding/src/runtime/repository-registry.js b/modules/jarvos-coding/src/runtime/repository-registry.js index 3a32f7c2..a9bff6bd 100644 --- a/modules/jarvos-coding/src/runtime/repository-registry.js +++ b/modules/jarvos-coding/src/runtime/repository-registry.js @@ -224,4 +224,27 @@ function recordOwnerAction({ registryPath, repositoryId, action, runId, revision return Object.freeze({ schemaVersion: 'jarvos-coding-owner-action-receipt/v1', action, repository: publicRepository(repository), runId, ...(revision ? { revision } : {}) }); } -module.exports = { REPOSITORY_REGISTRY_SCHEMA_VERSION, OWNER_ACTIONS_SCHEMA_VERSION, deriveRepositoryId, loadRepositoryRegistry, publicRepository, validateRepositoryRegistry, provisionRepository, inspectProvisionedRepositories, updateProvisionedRepository, revokeProvisionedRepository, recordOwnerAction }; +// This resolver is intentionally separate from the mutation helper above. A +// model-visible boundary may only derive acceptance evidence from this +// owner-written record; it must never accept caller-provided evidence. +function resolveOwnerPlanAcceptance({ registryPath, repositoryId, runId, planDigest, ownerUid, now = new Date() } = {}) { + if (typeof runId !== 'string' || !/^[A-Za-z0-9._:-]{1,160}$/.test(runId)) throw new Error('runId is required and must be an opaque identifier'); + if (typeof planDigest !== 'string' || !/^[a-f0-9]{64}$/i.test(planDigest)) throw new Error('planDigest must be a SHA-256 digest'); + const loaded = loadRepositoryRegistry(registryTarget(registryPath, { ownerUid }), { ownerUid }); + const repository = loaded.resolve(repositoryId); + const actionsPath = path.join(repository.stateRoot, 'owner-actions.json'); + if (!fs.existsSync(actionsPath)) return null; + assertOwner(fs.statSync(actionsPath), 'owner action record', ownerUid); + let actions; + try { actions = JSON.parse(fs.readFileSync(actionsPath, 'utf8')); } catch { throw new Error('owner action record is invalid'); } + if (!isObject(actions) || actions.schemaVersion !== OWNER_ACTIONS_SCHEMA_VERSION || actions.generation !== loaded.generation || !Array.isArray(actions.actions)) throw new Error('owner action record is invalid'); + const action = actions.actions.find((entry) => entry && entry.action === 'accept-plan' && entry.repositoryId === repository.repositoryId && entry.runId === runId); + if (!action || (action.revision !== planDigest && action.revision !== `sha256:${planDigest}`)) return null; + const observedAt = new Date(action.observedAt); + if (Number.isNaN(observedAt.getTime())) throw new Error('owner action record is invalid'); + const freshness = repository.acceptancePolicy.evidenceFreshnessMs; + if (freshness !== undefined && now.getTime() - observedAt.getTime() > freshness) return null; + return Object.freeze({ source: 'owner-action-record', observedAt: observedAt.toISOString(), planDigest }); +} + +module.exports = { REPOSITORY_REGISTRY_SCHEMA_VERSION, OWNER_ACTIONS_SCHEMA_VERSION, deriveRepositoryId, loadRepositoryRegistry, publicRepository, validateRepositoryRegistry, provisionRepository, inspectProvisionedRepositories, updateProvisionedRepository, revokeProvisionedRepository, recordOwnerAction, resolveOwnerPlanAcceptance }; diff --git a/modules/jarvos-coding/test/coding-mcp-pack.test.js b/modules/jarvos-coding/test/coding-mcp-pack.test.js new file mode 100644 index 00000000..b4a36429 --- /dev/null +++ b/modules/jarvos-coding/test/coding-mcp-pack.test.js @@ -0,0 +1,30 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); +const test = require('node:test'); + +const PACKAGE_ROOT = path.resolve(__dirname, '..'); + +test('packed coding MCP starts from an unpacked tarball without checkout-relative loading', () => { + const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'jarvos-coding-pack-')); + const packed = spawnSync('npm', ['pack', '--json', '--pack-destination', temporary], { cwd: PACKAGE_ROOT, encoding: 'utf8' }); + assert.equal(packed.status, 0, packed.stderr || packed.stdout); + const tarball = path.join(temporary, JSON.parse(packed.stdout.slice(packed.stdout.indexOf('[')))[0].filename); + const extracted = path.join(temporary, 'unpacked'); fs.mkdirSync(extracted); + const untar = spawnSync('tar', ['-xf', tarball, '-C', extracted], { encoding: 'utf8' }); + assert.equal(untar.status, 0, untar.stderr); + const script = path.join(extracted, 'package', 'scripts', 'jarvos-coding-mcp.js'); + assert.ok(fs.existsSync(script)); + const started = spawnSync(process.execPath, [script], { + cwd: temporary, + input: `${JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} })}\n`, + encoding: 'utf8', timeout: 5000, + }); + assert.equal(started.status, 0, started.stderr || started.stdout); + const reply = JSON.parse(started.stdout.trim()); + assert.equal(reply.result.serverInfo.name, 'jarvos-coding'); +}); diff --git a/modules/jarvos-coding/test/coding-mcp.test.js b/modules/jarvos-coding/test/coding-mcp.test.js new file mode 100644 index 00000000..43031824 --- /dev/null +++ b/modules/jarvos-coding/test/coding-mcp.test.js @@ -0,0 +1,67 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const test = require('node:test'); +const { TOOLS, callTool, handle } = require('../scripts/jarvos-coding-mcp'); + +const DIGEST = 'a'.repeat(64); +const packet = { version: 'jarvos-implementation-packet/v1', planDigest: DIGEST, steps: [{ id: 'step_1', description: 'Implement bounded change', files: ['src/example.js'] }] }; + +function fixture() { + const calls = []; + const workflow = Object.fromEntries(['plan', 'acceptPlan', 'work', 'finish', 'status', 'resume'].map((operation) => [operation, async (input) => { + calls.push([operation, input]); + return { ok: true, status: 'succeeded', workRunId: input.workRunId, root: '/private/root', credential: 'secret=hidden' }; + }])); + const runtime = { + listRepositories: () => [{ repositoryId: 'repo_fixture', label: 'Fixture', agentSelectable: true }], + health: () => ({ status: 'ok', root: '/private/root' }), + resolveRequest(input) { + assert.equal(input.repositoryId, 'repo_fixture'); + return { repositoryId: 'repo_fixture', subjectKey: 'repo_fixture:ORG-1', workRunId: input.workRunId || 'run_fixture', repository: { acceptancePolicy: { mode: 'human-evidence-required' } }, managedWorkflow: workflow }; + }, + }; + return { calls, options: { registryPath: '/host/registry.json', createRuntime: () => runtime, resolveOwnerAcceptance: () => ({ source: 'owner-action-record', observedAt: '2026-08-14T00:00:00.000Z', planDigest: DIGEST }) } }; +} +function result(response) { return JSON.parse(response.content[0].text); } + +test('public coding MCP exposes only managed operations', () => { + assert.deepEqual(TOOLS.map((tool) => tool.name), ['jarvos_coding_plan', 'jarvos_coding_accept_plan', 'jarvos_coding_work', 'jarvos_coding_finish', 'jarvos_coding_status', 'jarvos_coding_resume', 'jarvos_coding_repositories', 'jarvos_coding_health']); + assert.equal(TOOLS.some((tool) => /complete|compound/.test(tool.name)), false); +}); + +test('routes direct managed plan, acceptance, work, finish, status, and resume operations', async () => { + const f = fixture(); const base = { repositoryId: 'repo_fixture', subjectKey: 'ORG-1', workRunId: 'run_fixture' }; + await callTool('jarvos_coding_plan', { ...base, input: { kind: 'issue', digest: DIGEST } }, f.options); + await callTool('jarvos_coding_accept_plan', { ...base, planDigest: DIGEST, packet, artifact: { reference: 'artifact:plan_123456' } }, f.options); + await callTool('jarvos_coding_work', { ...base, planDigest: DIGEST, packet }, f.options); + await callTool('jarvos_coding_finish', { ...base, planDigest: DIGEST }, f.options); + await callTool('jarvos_coding_status', base, f.options); + const response = await callTool('jarvos_coding_resume', base, f.options); + assert.deepEqual(f.calls.map(([operation]) => operation), ['plan', 'acceptPlan', 'work', 'finish', 'status', 'resume']); + assert.equal(f.calls[1][1].acceptanceEvidence.source, 'owner-action-record'); + assert.equal(f.calls[3][1].issueIdentifier, 'ORG-1'); + assert.equal(f.calls[3][1].subjectKey, 'repo_fixture:ORG-1'); + assert.equal(JSON.stringify(result(response)).includes('/private/root'), false); + assert.equal(JSON.stringify(result(response)).includes('secret=hidden'), false); +}); + +test('fails closed for malformed, unknown, and model-supplied authority input', async () => { + const f = fixture(); const base = { repositoryId: 'repo_fixture', subjectKey: 'ORG-1' }; + await assert.rejects(() => callTool('jarvos_coding_status', { ...base, root: '/tmp/nope' }, f.options), /not allowed/); + await assert.rejects(() => callTool('jarvos_coding_work', { ...base, planDigest: DIGEST, packet: { ...packet, credential: 'x' } }, f.options), /not allowed/); + await assert.rejects(() => callTool('jarvos_coding_unknown', base, f.options), /Unknown tool/); + await assert.rejects(() => callTool('jarvos_coding_plan', { ...base, input: { kind: 'issue', digest: 'bad' } }, f.options), /digest/); +}); + +test('handles initialize, tools/list, and tools/call with JSON-RPC', async () => { + const written = []; const original = process.stdout.write; process.stdout.write = (line) => { written.push(JSON.parse(line)); return true; }; + try { + await handle({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }); + await handle({ jsonrpc: '2.0', id: 2, method: 'tools/list' }); + await handle({ jsonrpc: '2.0', id: 3, method: 'tools/call', params: { name: 'jarvos_coding_repositories', arguments: {} } }, fixture().options); + } finally { process.stdout.write = original; } + assert.equal(written[0].result.serverInfo.name, 'jarvos-coding'); + assert.equal(written[1].result.tools.length, TOOLS.length); + assert.equal(result(written[2].result).repositories[0].repositoryId, 'repo_fixture'); +}); diff --git a/modules/jarvos-coding/test/repository-provisioning.test.js b/modules/jarvos-coding/test/repository-provisioning.test.js index bf7eb80d..e0a41b6f 100644 --- a/modules/jarvos-coding/test/repository-provisioning.test.js +++ b/modules/jarvos-coding/test/repository-provisioning.test.js @@ -11,6 +11,7 @@ const { updateProvisionedRepository, revokeProvisionedRepository, recordOwnerAction, + resolveOwnerPlanAcceptance, loadRepositoryRegistry, } = require('../src'); @@ -70,6 +71,16 @@ test('owner actions are scoped to one provisioned repository and plan revision', assert.throws(() => recordOwnerAction({ registryPath: f.registryPath, repositoryId: 'missing', action: 'decline-learning', runId: 'run_1' }), /unknown repository/); }); +test('owner plan acceptance resolves only a durable matching record', () => { + const f = fixture(); + const added = provisionRepository({ registryPath: f.registryPath, repository: f.entry }); + const digest = 'a'.repeat(64); + recordOwnerAction({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, action: 'accept-plan', runId: 'run_1', revision: digest }); + const evidence = resolveOwnerPlanAcceptance({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, runId: 'run_1', planDigest: digest }); + assert.deepEqual({ source: evidence.source, planDigest: evidence.planDigest }, { source: 'owner-action-record', planDigest: digest }); + assert.equal(resolveOwnerPlanAcceptance({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, runId: 'run_1', planDigest: 'b'.repeat(64) }), null); +}); + test('provisioning fails closed for missing explicit authority and unsafe roots', () => { const f = fixture(); assert.throws(() => provisionRepository({ registryPath: f.registryPath, repository: { ...f.entry, acceptancePolicy: undefined } }), /acceptancePolicy is required/); diff --git a/modules/jarvos-skills/manifest.json b/modules/jarvos-skills/manifest.json index 35ff01ac..a88ffcdb 100644 --- a/modules/jarvos-skills/manifest.json +++ b/modules/jarvos-skills/manifest.json @@ -31,18 +31,19 @@ { "name": "workflow-execution", "path": "skills/workflow-execution/SKILL.md", - "purpose": "Plan, track, package, execute, and verify non-trivial work.", + "purpose": "Plan, accept, work, finish, inspect, and resume jarvOS-managed coding runs.", "source": { "revision": "jarvos-skills-v0.3.0", - "digest": "32ad169ce1f2296433fd552be6b0c6251d69b9e28ea620b97123e9d9310e7c04", + "digest": "da13d968a2305dfe44c49f40fd21d2e2fae13a1a6036432d8b0d44a6ff5775d3", "license": "MIT", "provenance": "jarvOS reviewed source" }, - "supportedHarnesses": ["generic", "hermes"], + "supportedHarnesses": ["generic", "codex", "hermes"], "projection": { "mode": "copy", "targets": { "generic": { "path": "{skillsRoot}/workflow-execution/SKILL.md", "renderer": "raw-skill-md" }, + "codex": { "path": "{skillsRoot}/workflow-execution/SKILL.md", "renderer": "raw-skill-md" }, "hermes": { "path": "{skillsRoot}/workflow-execution/SKILL.md", "renderer": "raw-skill-md" } } } diff --git a/modules/jarvos-skills/skills/workflow-execution/SKILL.md b/modules/jarvos-skills/skills/workflow-execution/SKILL.md index 4b51b8e5..89fde789 100644 --- a/modules/jarvos-skills/skills/workflow-execution/SKILL.md +++ b/modules/jarvos-skills/skills/workflow-execution/SKILL.md @@ -1,6 +1,6 @@ --- name: workflow-execution -description: Plan-first workflow for non-trivial work: define the goal, create or reuse a tracker issue, package context, execute on an issue-named branch when code changes, and verify completion with evidence. +description: Plan-first jarvOS workflow for non-trivial coding work. Use it for planning, implementing, resuming, checking status, or finishing a managed coding run; it calls the jarvOS coding MCP tools and does not require Compound Engineering commands. triggers: - make a plan - plan this @@ -13,6 +13,8 @@ metadata: bundle: operating-system-skills portability: generic managedCodingProvider: compound-engineering + codex: + implicitInvocation: managed coding intent only --- # Workflow Execution @@ -51,21 +53,32 @@ The workflow is complete only when: 8. **Close or hand off.** Move the issue to done only when no follow-up remains. Use in-review only when a real reviewer path exists. -## Managed coding verbs - -When this skill is running inside a jarvOS coding profile, the natural verbs -`plan`, `work`, and `complete` use the jarvOS-managed provider route. A healthy, -approved Compound Engineering provider supplies the planning and implementation -discipline behind the scenes; jarvOS still owns the work-run, branch/worktree, -accepted plan revision, review evidence, submission gate, and completion -decision. `compound` is an explicit, post-verification learning-capture step, -not a substitute for completion evidence. - -If the provider is unavailable, modified, unsupported, or fails during a run, -fall back through the generic workflow in the same work run and worktree. Do -not start a second plan, branch, or pull request. Treat provider checkpoints as -reattachment hints only and revalidate current Git, review, test, and PR -evidence before claiming completion. +## Managed coding workflow (Codex) + +In a jarvOS-managed Codex profile, use the `jarvos-coding` MCP tools for ordinary +coding intent: plan, accept the plan when the repository policy allows it, work, +finish, check status, or resume. Start with `jarvos_coding_repositories` when a +repository identifier is needed, then use this sequence for one subject: + +1. `jarvos_coding_plan` +2. `jarvos_coding_accept_plan` +3. `jarvos_coding_work` +4. `jarvos_coding_finish` + +Use `jarvos_coding_status` or `jarvos_coding_resume` to continue an existing +run. The MCP contract accepts only an owner-provisioned opaque repository ID; +never supply or infer a repository root, executable, credential, provider, or +registry path from the request. The deterministic direct invocation is the +recovery path when implicit skill selection does not occur. + +jarvOS owns the durable run, accepted plan revision, canonical worktree, +verification, and completion decision. If the approved managed provider is +unavailable, modified, unsupported, or fails, jarvOS uses its bounded native +coding fallback in that same managed run and worktree. Do not start a second +plan, branch, or pull request. Compound Engineering is an implementation detail; +users do not need to invoke it manually. Automatic learning capture is guaranteed +only after eligible jarvOS-managed runs that reach jarvOS-verified completion, +not for arbitrary unmanaged Codex edits. ## Definition of done template diff --git a/modules/jarvos-skills/test/projection.test.js b/modules/jarvos-skills/test/projection.test.js index b74e56d3..674302d4 100644 --- a/modules/jarvos-skills/test/projection.test.js +++ b/modules/jarvos-skills/test/projection.test.js @@ -29,7 +29,9 @@ try { const first = planSkillProjection({ harness: 'hermes', skillsRoot: root, skills: ['workflow-execution'] }); assert.equal(first.entries[0].status, 'missing'); assert.equal(first.entries[0].action, 'create'); - assert.equal(planSkillProjection({ harness: 'codex', skillsRoot: root, skills: ['workflow-execution'] }).entries[0].status, 'unsupported'); + const codexWorkflow = planSkillProjection({ harness: 'codex', skillsRoot: root, skills: ['workflow-execution'] }); + assert.equal(codexWorkflow.entries[0].status, 'missing'); + assert.equal(codexWorkflow.entries[0].targetPath, path.join(fs.realpathSync(root), 'workflow-execution', 'SKILL.md')); assert.equal(planSkillProjection({ harness: 'hermes', skillsRoot: root, skills: ['workflow-execution'], incompatibleSkills: ['workflow-execution'] }).entries[0].status, 'incompatible'); const initial = applySkillProjection(first); assert.equal(initial.applied[0].applied, true); @@ -168,6 +170,22 @@ try { fs.rmSync(harnessRoot, { recursive: true, force: true }); } + const codexAdapter = JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', '..', 'runtimes', 'codex', 'adapter.json'), 'utf8')); + assert.equal(codexAdapter.codingWorkflow.mcpServer, 'modules/jarvos-coding/scripts/jarvos-coding-mcp.js'); + assert.deepEqual(codexAdapter.codingWorkflow.toolSequence, [ + 'jarvos_coding_plan', 'jarvos_coding_accept_plan', 'jarvos_coding_work', + 'jarvos_coding_finish', 'jarvos_coding_status', 'jarvos_coding_resume', + ]); + assert.ok(codexAdapter.setup.states.includes('installed-but-unwired')); + assert.ok(codexAdapter.setup.states.includes('native-fallback-ready')); + + const codexSetup = fs.readFileSync(path.join(__dirname, '..', '..', '..', 'runtimes', 'codex', 'setup.sh'), 'utf8'); + assert.match(codexSetup, /CODING_REGISTRY="\$\{JARVOS_CODING_REGISTRY:-\}"/); + assert.match(codexSetup, /loadRepositoryRegistry\(process\.argv\[3\], \{ ownerUid: process\.getuid\?\.\(\) \}\)/); + assert.match(codexSetup, /codex mcp add --env "JARVOS_CODING_REPOSITORY_REGISTRY=\$CODING_REGISTRY" jarvos-coding/); + assert.match(codexSetup, /planSkillProjection\(\{ harness: 'codex', skillsRoot, skills: \['workflow-execution'\] \}\)/); + assert.doesNotMatch(codexSetup, /--env "JARVOS_CODING_REGISTRY=/); + const packageRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'jarvos-skills-package-')); const stagingRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'jarvos-skills-staging-')); fs.mkdirSync(path.join(packageRoot, 'skill')); diff --git a/package.json b/package.json index 75681d67..47cd4f07 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "modules/jarvos-coding/package.json", "modules/jarvos-coding/src/", "modules/jarvos-coding/providers/", + "modules/jarvos-coding/scripts/", "modules/jarvos-control-plane/README.md", "modules/jarvos-control-plane/package.json", "modules/jarvos-control-plane/scripts/", diff --git a/runtimes/codex/adapter.json b/runtimes/codex/adapter.json index 51ed47ce..5ea9629d 100644 --- a/runtimes/codex/adapter.json +++ b/runtimes/codex/adapter.json @@ -38,6 +38,15 @@ "jarvos_control_plane" ] }, + "codingWorkflow": { + "mcpServer": "modules/jarvos-coding/scripts/jarvos-coding-mcp.js", + "registration": "codex mcp add --env JARVOS_CODING_REPOSITORY_REGISTRY= jarvos-coding -- node ", + "registryBinding": "JARVOS_CODING_REGISTRY is setup input only; setup validates its absolute owner-only path and persists it only as JARVOS_CODING_REPOSITORY_REGISTRY for the jarvos-coding server.", + "skill": "workflow-execution", + "toolSequence": ["jarvos_coding_plan", "jarvos_coding_accept_plan", "jarvos_coding_work", "jarvos_coding_finish", "jarvos_coding_status", "jarvos_coding_resume"], + "fallback": "jarvos-native-workflow", + "managedRunBoundary": "Applies only after the jarvOS coding tool creates or resumes a managed run; implicit skill selection does not intercept arbitrary Codex edits." + }, "targets": [ { "id": "codex-cli", @@ -69,7 +78,9 @@ } ], "setup": { - "script": "setup.sh" + "script": "setup.sh", + "states": ["installed-but-unwired", "managed-provider-ready", "native-fallback-ready", "disabled", "outdated", "blocked"], + "updatePolicy": "Update jarvOS-owned MCP registrations and projections only; preserve unrelated Codex configuration and locally modified projected skills." }, "managedProviders": { "compound-engineering": { diff --git a/runtimes/codex/setup.sh b/runtimes/codex/setup.sh index 9a2e8292..6d0bd251 100755 --- a/runtimes/codex/setup.sh +++ b/runtimes/codex/setup.sh @@ -3,11 +3,13 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" MCP_SERVER="$ROOT/modules/jarvos-agent-context/scripts/jarvos-mcp.js" +CODING_MCP_SERVER="$ROOT/modules/jarvos-coding/scripts/jarvos-coding-mcp.js" MANAGED_HOOKS_JSON="$ROOT/runtimes/codex/hooks.json" HOOK_SCRIPT="$ROOT/runtimes/codex/jarvos-session-start-hook.js" TURN_HOOK_SCRIPT="$ROOT/runtimes/codex/jarvos-session-turn-hook.js" TRUST_SCRIPT="$ROOT/runtimes/codex/trust-session-start-hook.js" CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" +SKILLS_ROOT="$CODEX_HOME/skills" CODEX_CONFIG="${CODEX_CONFIG:-$CODEX_HOME/config.toml}" LEGACY_HOOKS_JSON="$CODEX_HOME/hooks.json" CONTROL_PLANE_SERVICE_MODULE="${JARVOS_CONTROL_PLANE_SERVICE_MODULE:-}" @@ -19,6 +21,10 @@ STEWARDSHIP_CODEX_SESSION_MAP_ROOT="${JARVOS_STEWARDSHIP_CODEX_SESSION_MAP_ROOT: STEWARDSHIP_STABLE_ROOT="${JARVOS_STEWARDSHIP_STABLE_ROOT:-}" STEWARDSHIP_DISPATCHER="" CODEX_PROVIDER_MODE="${JARVOS_CODEX_PROVIDER_MODE:-}" +# This is setup input only. The coding MCP receives the validated path through +# its fixed JARVOS_CODING_REPOSITORY_REGISTRY binding; no roots, credentials, +# providers, or executable configuration are registered from model input. +CODING_REGISTRY="${JARVOS_CODING_REGISTRY:-}" # The private installer materializes this owner-controlled bundle once. Native # configuration must refer to it, never to a selected immutable runtime stage. @@ -52,11 +58,27 @@ if [ ! -f "$MCP_SERVER" ]; then exit 1 fi +if [ ! -f "$CODING_MCP_SERVER" ]; then + echo "jarvOS coding MCP server not found: $CODING_MCP_SERVER" >&2 + exit 1 +fi + if [ ! -f "$MANAGED_HOOKS_JSON" ]; then echo "jarvOS Codex hooks config not found: $MANAGED_HOOKS_JSON" >&2 exit 1 fi +if [ -n "$CODING_REGISTRY" ]; then + if ! node - "$ROOT/modules/jarvos-coding/src/runtime/repository-registry.js" "$CODING_REGISTRY" <<'NODE' +const { loadRepositoryRegistry } = require(process.argv[2]); +loadRepositoryRegistry(process.argv[3], { ownerUid: process.getuid?.() }); +NODE + then + echo "JARVOS_CODING_REGISTRY must be a valid absolute owner-only provisioned registry" >&2 + exit 1 + fi +fi + if [ ! -f "$HOOK_SCRIPT" ]; then echo "jarvOS Codex hook script not found: $HOOK_SCRIPT" >&2 exit 1 @@ -157,6 +179,32 @@ if [ "${JARVOS_STEWARDSHIP_ONLY:-0}" != "1" ]; then codex mcp add jarvos -- node "$MCP_SERVER" echo "Registered jarvOS MCP server for Codex: $MCP_SERVER" fi + + if [ "${JARVOS_MANAGED_HARNESS_ROLLBACK:-0}" = "1" ]; then + codex mcp remove jarvos-coding >/dev/null 2>&1 || true + echo "Removed jarvOS-owned coding MCP registration for Codex." + elif [ -n "$CODING_REGISTRY" ]; then + if codex mcp get jarvos-coding >/dev/null 2>&1; then + codex mcp remove jarvos-coding >/dev/null + fi + codex mcp add --env "JARVOS_CODING_REPOSITORY_REGISTRY=$CODING_REGISTRY" jarvos-coding -- node "$CODING_MCP_SERVER" + echo "Registered jarvOS coding MCP server for Codex with an owner-bound repository registry." + else + echo "jarvOS coding MCP remains installed-but-unwired; provision a registry and rerun setup with JARVOS_CODING_REGISTRY." + fi +fi + +# Projections own only their receipt and target. A locally modified target is +# deliberately preserved by applySkillProjection; setup never overwrites it. +if [ "${JARVOS_MANAGED_HARNESS_ROLLBACK:-0}" != "1" ]; then + node - "$ROOT/modules/jarvos-skills" "$SKILLS_ROOT" <<'NODE' +const { applySkillProjection, planSkillProjection } = require(process.argv[2]); +const skillsRoot = process.argv[3]; +const plan = planSkillProjection({ harness: 'codex', skillsRoot, skills: ['workflow-execution'] }); +const result = applySkillProjection(plan); +const entry = result.applied[0]; +console.log(entry.applied ? 'Projected jarvOS workflow-execution skill for Codex.' : `Preserved Codex workflow-execution skill (${entry.status}).`); +NODE fi mkdir -p "$(dirname "$CODEX_CONFIG")" diff --git a/tests/pack-manifest-test.js b/tests/pack-manifest-test.js index e18eaffc..5c7cf612 100644 --- a/tests/pack-manifest-test.js +++ b/tests/pack-manifest-test.js @@ -50,6 +50,7 @@ function advertisedRuntimeAssets() { 'modules/jarvos-skills/schemas/local-overlay.schema.json', 'modules/jarvos-skills/src/reconciliation.js', 'modules/jarvos-skills/scripts/dogfood-skills.js', + 'modules/jarvos-coding/scripts/jarvos-coding-mcp.js', 'modules/jarvos-control-plane/scripts/jarvos-manager.js', 'scripts/release-readiness-check.js', 'scripts/release-status.js', From 9ed17833120027870ba6d2f1f651ff65d36c906b Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 15:12:30 -0400 Subject: [PATCH 05/12] feat(codex): validate managed coding activation --- modules/jarvos-runtime-kit/src/index.js | 71 +++++++++++++++++++ .../test/runtime-kit.test.js | 14 ++++ modules/jarvos/src/doctor.js | 28 ++++++++ tests/doctor-checks-test.js | 18 +++++ 4 files changed, 131 insertions(+) diff --git a/modules/jarvos-runtime-kit/src/index.js b/modules/jarvos-runtime-kit/src/index.js index 8dad5515..e3ed2045 100644 --- a/modules/jarvos-runtime-kit/src/index.js +++ b/modules/jarvos-runtime-kit/src/index.js @@ -16,6 +16,17 @@ const DEFAULT_AGENT_CONTEXT_MCP = 'modules/jarvos-agent-context/scripts/jarvos-m const REQUIRED_MCP_TOOL = 'jarvos_hydrate'; const CONTROL_PLANE_TOOL = 'jarvos_control_plane'; const CONTROL_PLANE_MODULE = 'modules/jarvos-control-plane/scripts/jarvos-manager.js'; +const CODEX_CODING_MCP = 'modules/jarvos-coding/scripts/jarvos-coding-mcp.js'; +const CODEX_CODING_TOOLS = [ + 'jarvos_coding_plan', 'jarvos_coding_accept_plan', 'jarvos_coding_work', + 'jarvos_coding_finish', 'jarvos_coding_status', 'jarvos_coding_resume', + 'jarvos_coding_repositories', 'jarvos_coding_health', +]; +const CODEX_CODING_SEQUENCE = CODEX_CODING_TOOLS.slice(0, 6); +const CODEX_CODING_PROFILE_STATES = [ + 'installed-but-unwired', 'managed-provider-ready', 'native-fallback-ready', + 'disabled', 'outdated', 'blocked', +]; const HYDRATION_MODES = ['hook', 'manual', 'unsupported']; const COMPOUND_ENGINEERING_CAPABILITY_VERSION = 'jarvos-codex-ce-capability.v1'; const COMPOUND_ENGINEERING_OPERATIONS = ['plan', 'work', 'compound']; @@ -705,6 +716,59 @@ function sourceContains(filePath, patterns) { return patterns.some((pattern) => pattern.test(content)); } +function validateCodexCodingWorkflow(manifest, options = {}) { + const root = path.resolve(options.root || repoRootFrom()); + const setupScript = options.setupScript || path.join(root, 'runtimes', 'codex', 'setup.sh'); + const errors = []; + const workflow = manifest?.codingWorkflow; + if (!isObject(workflow)) return { ok: false, errors: ['Codex codingWorkflow is required'] }; + if (workflow.mcpServer !== CODEX_CODING_MCP) errors.push(`Codex codingWorkflow.mcpServer must be ${CODEX_CODING_MCP}`); + const mcpPath = path.join(root, workflow.mcpServer || ''); + if (!fs.existsSync(mcpPath)) { + errors.push('Codex coding MCP server is missing'); + } else { + try { + const mcp = require(mcpPath); + const names = Array.isArray(mcp.TOOLS) ? mcp.TOOLS.map((tool) => tool.name) : []; + if (names.length !== CODEX_CODING_TOOLS.length || names.some((name, index) => name !== CODEX_CODING_TOOLS[index])) { + errors.push(`Codex coding MCP tools must be exactly: ${CODEX_CODING_TOOLS.join(', ')}`); + } + } catch (error) { + errors.push(`Codex coding MCP server could not be loaded: ${error.message}`); + } + } + if (typeof workflow.registration !== 'string' || !workflow.registration.includes('JARVOS_CODING_REPOSITORY_REGISTRY')) { + errors.push('Codex codingWorkflow.registration must bind JARVOS_CODING_REPOSITORY_REGISTRY'); + } + if (typeof workflow.registration === 'string' && /(?:^|[^A-Z0-9_])JARVOS_CODING_REGISTRY(?:$|[^A-Z0-9_])/.test(workflow.registration)) { + errors.push('Codex codingWorkflow.registration must not bind raw JARVOS_CODING_REGISTRY'); + } + if (!Array.isArray(workflow.toolSequence) || workflow.toolSequence.length !== CODEX_CODING_SEQUENCE.length + || workflow.toolSequence.some((name, index) => name !== CODEX_CODING_SEQUENCE[index])) { + errors.push(`Codex codingWorkflow.toolSequence must be exactly: ${CODEX_CODING_SEQUENCE.join(', ')}`); + } + const states = manifest?.setup?.states; + if (!Array.isArray(states) || states.length !== CODEX_CODING_PROFILE_STATES.length + || states.some((state, index) => state !== CODEX_CODING_PROFILE_STATES[index])) { + errors.push(`Codex setup.states must be exactly: ${CODEX_CODING_PROFILE_STATES.join(', ')}`); + } + if (!fs.existsSync(setupScript)) { + errors.push('Codex coding setup script is missing'); + } else { + const setup = fs.readFileSync(setupScript, 'utf8'); + if (!/CODING_REGISTRY="\$\{JARVOS_CODING_REGISTRY:-\}"/.test(setup) || !/loadRepositoryRegistry\(process\.argv\[3\], \{ ownerUid: process\.getuid\?\.\(\) \}\)/.test(setup)) { + errors.push('Codex coding setup must validate the owner-bound JARVOS_CODING_REGISTRY input'); + } + if (!/--env "JARVOS_CODING_REPOSITORY_REGISTRY=\$CODING_REGISTRY" jarvos-coding/.test(setup)) { + errors.push('Codex coding setup must bind only JARVOS_CODING_REPOSITORY_REGISTRY to jarvos-coding'); + } + if (/--env "JARVOS_CODING_REGISTRY=/.test(setup)) { + errors.push('Codex coding setup must not persist raw JARVOS_CODING_REGISTRY'); + } + } + return { ok: errors.length === 0, errors }; +} + function checkRuntime(manifestPath, options = {}) { const root = path.resolve(options.root || repoRootFrom()); const loaded = loadManifest(path.isAbsolute(manifestPath) ? manifestPath : path.join(root, manifestPath)); @@ -801,6 +865,8 @@ function checkRuntime(manifestPath, options = {}) { } if (manifest.id === 'codex') { + const codingWorkflow = validateCodexCodingWorkflow(manifest, { root, setupScript }); + if (!codingWorkflow.ok) for (const error of codingWorkflow.errors) add(errors, error); const capabilityPath = path.join(runtimeDir, 'compound-engineering-capability.json'); let capabilityRecord = null; if (!fs.existsSync(capabilityPath)) { @@ -886,6 +952,10 @@ module.exports = { REQUIRED_MCP_TOOL, CONTROL_PLANE_MODULE, CONTROL_PLANE_TOOL, + CODEX_CODING_MCP, + CODEX_CODING_PROFILE_STATES, + CODEX_CODING_SEQUENCE, + CODEX_CODING_TOOLS, COMPOUND_ENGINEERING_CAPABILITY_VERSION, checkCompoundEngineeringCapability, classifyCompoundEngineeringProvider, @@ -900,5 +970,6 @@ module.exports = { scaffoldRuntime, validateCompoundEngineeringCapability, validateCodexConformanceReceipt, + validateCodexCodingWorkflow, validateManifest, }; diff --git a/modules/jarvos-runtime-kit/test/runtime-kit.test.js b/modules/jarvos-runtime-kit/test/runtime-kit.test.js index 3533d3b5..57c0fe2e 100644 --- a/modules/jarvos-runtime-kit/test/runtime-kit.test.js +++ b/modules/jarvos-runtime-kit/test/runtime-kit.test.js @@ -10,6 +10,7 @@ const test = require('node:test'); const { COMPOUND_ENGINEERING_CAPABILITY_VERSION, checkRuntime, + validateCodexCodingWorkflow, checkCompoundEngineeringCapability, classifyCompoundEngineeringProvider, computeCompoundEngineeringFixtureDigest, @@ -569,6 +570,19 @@ test('checkRuntime passes every checked-in adapter manifest', () => { } }); +test('Codex coding workflow requires the public MCP schema and owner-only setup binding', () => { + const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'runtimes/codex/adapter.json'), 'utf8')); + assert.equal(validateCodexCodingWorkflow(manifest, { root: ROOT }).ok, true); + + const unsafeRegistration = JSON.parse(JSON.stringify(manifest)); + unsafeRegistration.codingWorkflow.registration = 'codex mcp add --env JARVOS_CODING_REGISTRY= jarvos-coding'; + assert.match(validateCodexCodingWorkflow(unsafeRegistration, { root: ROOT }).errors.join('\n'), /JARVOS_CODING_REPOSITORY_REGISTRY/); + + const incompleteStates = JSON.parse(JSON.stringify(manifest)); + incompleteStates.setup.states = ['installed-but-unwired']; + assert.match(validateCodexCodingWorkflow(incompleteStates, { root: ROOT }).errors.join('\n'), /setup\.states/); +}); + test('checkRuntime reports unloadable MCP servers without throwing', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'jarvos-runtime-kit-bad-mcp-')); try { diff --git a/modules/jarvos/src/doctor.js b/modules/jarvos/src/doctor.js index afebd212..a4af96c4 100644 --- a/modules/jarvos/src/doctor.js +++ b/modules/jarvos/src/doctor.js @@ -142,6 +142,33 @@ function createCheck(component, ok, message, details = {}) { }; } +const CODEX_CODING_PROFILE_STATES = new Set([ + 'installed-but-unwired', 'managed-provider-ready', 'native-fallback-ready', + 'disabled', 'outdated', 'blocked', +]); + +function validateCodexCodingWorkflow(evidence = {}) { + // This helper intentionally accepts only booleans and returns no host paths, + // registry values, credential references, or provider configuration. + const state = evidence.disabled === true ? 'disabled' + : evidence.outdated === true ? 'outdated' + : evidence.blocked === true ? 'blocked' + : evidence.registryBound !== true ? 'installed-but-unwired' + : evidence.managedProviderReady === true ? 'managed-provider-ready' + : evidence.nativeFallbackReady === true ? 'native-fallback-ready' + : 'blocked'; + if (!CODEX_CODING_PROFILE_STATES.has(state)) throw new Error('Codex coding profile state is invalid'); + const messages = { + 'installed-but-unwired': 'Codex workflow skill is installed, but no owner-provisioned coding registry is bound.', + 'managed-provider-ready': 'Codex managed coding workflow is ready with its approved provider route.', + 'native-fallback-ready': 'Codex managed coding workflow is ready with its native fallback route.', + disabled: 'Codex managed coding workflow is disabled by its owner-controlled profile state.', + outdated: 'Codex managed coding workflow is outdated; rerun supported setup before starting a managed run.', + blocked: 'Codex managed coding workflow is blocked; inspect the public setup and doctor guidance before retrying.', + }; + return createCheck('codex.codingWorkflow', ['installed-but-unwired', 'managed-provider-ready', 'native-fallback-ready', 'disabled'].includes(state), messages[state], { status: state }); +} + function getPathConfig(config, key) { if (!config || typeof config !== 'object') return undefined; if (!config.paths || typeof config.paths !== 'object') return undefined; @@ -1277,4 +1304,5 @@ module.exports = { validateObsidianPaths, validateObsidianSingleWriter, validateCompoundEngineeringProvider, + validateCodexCodingWorkflow, }; diff --git a/tests/doctor-checks-test.js b/tests/doctor-checks-test.js index 8e2adf12..e98cdbf0 100644 --- a/tests/doctor-checks-test.js +++ b/tests/doctor-checks-test.js @@ -19,10 +19,28 @@ const { checkJournalConflict, } = require('../lib/jarvos-cli'); const { + validateCodexCodingWorkflow, validateJarvosProfile, validateOpenClawProfile, } = require('../modules/jarvos/src/doctor'); +test('Codex coding doctor reports only normative public-safe profile states', () => { + const cases = [ + [{}, 'installed-but-unwired', true], + [{ registryBound: true, managedProviderReady: true }, 'managed-provider-ready', true], + [{ registryBound: true, nativeFallbackReady: true }, 'native-fallback-ready', true], + [{ disabled: true }, 'disabled', true], + [{ outdated: true }, 'outdated', false], + [{ blocked: true }, 'blocked', false], + ]; + for (const [evidence, status, ok] of cases) { + const result = validateCodexCodingWorkflow(evidence); + assert.equal(result.status, status); + assert.equal(result.ok, ok); + assert.doesNotMatch(JSON.stringify(result), /\/Users\/|secret|credential|registry\.json/i); + } +}); + function scratch() { return fs.mkdtempSync(path.join(os.tmpdir(), 'jarvos-doctor-')); } From e8f31f3c9013c7222ee7bf6b2462c8f12b69ade6 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 15:17:53 -0400 Subject: [PATCH 06/12] test(codex): prove packaged coding lifecycle --- .../test/codex-coding-conformance.test.js | 223 ++++++++++++++++++ .../codex/coding-lifecycle-conformance.json | 37 +++ tests/pack-manifest-test.js | 14 ++ 3 files changed, 274 insertions(+) create mode 100644 modules/jarvos-runtime-kit/test/codex-coding-conformance.test.js create mode 100644 runtimes/codex/coding-lifecycle-conformance.json diff --git a/modules/jarvos-runtime-kit/test/codex-coding-conformance.test.js b/modules/jarvos-runtime-kit/test/codex-coding-conformance.test.js new file mode 100644 index 00000000..931c4e8e --- /dev/null +++ b/modules/jarvos-runtime-kit/test/codex-coding-conformance.test.js @@ -0,0 +1,223 @@ +'use strict'; + +// Deterministic, clean-profile proof for the *public* coding MCP surface. +// The provider and tracker adapters are deliberately local doubles: this test +// does not claim that a hosted Codex/provider session was observed. +const assert = require('node:assert/strict'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); +const test = require('node:test'); + +const ROOT = path.resolve(__dirname, '../../..'); +const coding = require(path.join(ROOT, 'modules/jarvos-coding/src')); +const { handle } = require(path.join(ROOT, 'modules/jarvos-coding/scripts/jarvos-coding-mcp')); + +const DIGEST = 'c'.repeat(64); +const RUN_ID = 'run_conformance_01'; +const SUBJECT = 'CONFORM-1'; +const packet = { version: 'jarvos-implementation-packet/v1', planDigest: DIGEST, summary: 'Apply the accepted fixture change.', steps: [{ id: 'step_01', description: 'Update the public fixture.', files: ['fixture.txt'] }] }; + +function sha(value) { return crypto.createHash('sha256').update(value).digest('hex'); } +function run(command, args, cwd) { + const result = spawnSync(command, args, { cwd, encoding: 'utf8' }); + assert.equal(result.status, 0, result.stderr || result.stdout); + return result.stdout.trim(); +} +function providerSnapshot() { + const manifest = require(path.join(ROOT, 'modules/jarvos-coding/providers/compound-engineering.json')); + return { id: manifest.id, version: manifest.version, pinDigest: manifest.source.contentDigest, harness: 'codex', adapterVersion: manifest.harnesses.codex.adapter, status: 'verified' }; +} +function providerReceipt(invocation, operation) { + return { + version: 'jarvos-workflow-provider-receipt/v1', operation, status: 'succeeded', + workRunId: invocation.workRunId, operationNonce: invocation.operationNonce, idempotencyKey: invocation.idempotencyKey, + provider: invocation.provider, artifact: { kind: operation, reference: `artifact:${operation}_fixture_001`, path: `/var/lib/jarvos/${operation}.json`, digest: operation === 'plan' ? DIGEST : sha(operation) }, + planRevisionDigest: operation === 'plan' ? DIGEST : null, + acceptedPlanDigest: operation === 'plan' ? null : DIGEST, + publicLabel: `Fixture ${operation} receipt`, diagnostics: [], + }; +} +function successfulAdapters() { + return { + reviewEngine: { + sliceReview: async () => ({ status: 'passed', artifact: 'slice.json', summary: 'fixture review' }), + holisticReview: async () => ({ status: 'passed', artifact: 'holistic.json', summary: 'fixture review' }), + }, + tracker: { + claimIssue: async () => ({ status: 'claimed', ok: true, workReference: { authority: 'fixture', itemId: SUBJECT } }), + verifyAndClose: async () => ({ status: 'closed', ok: true, liveConfirmed: true }), + }, + git: { createBranch: async ({ branch }) => ({ status: 'created', branch, ok: true }) }, + fixer: { fixAndRerun: async () => ({ status: 'passed', ok: true, git: { clean: true, status: 'clean' } }) }, + pullRequest: { openPullRequest: async () => ({ status: 'created', url: 'https://example.test/pr/1', state: 'MERGED', ok: true }) }, + postMerge: { sweep: async () => ({ status: 'completed', ok: true }) }, + }; +} + +function fixture() { + const base = fs.mkdtempSync(path.join(os.tmpdir(), 'jarvos-coding-conformance-')); + const repository = path.join(base, 'fixture-repository'); + const stateRoot = path.join(base, 'state'); + const worktreeRoot = path.join(base, 'worktrees'); + const codexHome = path.join(base, 'codex-home'); + const registryPath = path.join(base, 'registry.json'); + fs.mkdirSync(repository, { recursive: true, mode: 0o700 }); + fs.mkdirSync(codexHome, { recursive: true, mode: 0o700 }); + run('git', ['init', '--initial-branch=main'], repository); + run('git', ['config', 'user.email', 'fixture@example.test'], repository); + run('git', ['config', 'user.name', 'Public Fixture'], repository); + fs.writeFileSync(path.join(repository, 'fixture.txt'), 'fixture\n'); + run('git', ['add', 'fixture.txt'], repository); + run('git', ['commit', '-m', 'fixture'], repository); + const provisioned = coding.provisionRepository({ registryPath, repository: { + publicLabel: 'Conformance fixture', agentSelectable: true, root: repository, stateRoot, + worktreePolicy: { root: worktreeRoot }, tracker: { kind: 'fixture' }, + acceptancePolicy: { mode: 'human-evidence-required' }, providerEgressPolicy: { classes: ['plan'] }, credentialReferences: {}, + learning: { enabled: true }, learningPublicationTarget: 'fixture:learning', + } }); + return { base, repository, stateRoot, worktreeRoot, codexHome, registryPath, repositoryId: provisioned.repository.repositoryId, nativeWorkCalls: 0, learningCalls: 0 }; +} + +function runtimeFor(f, behavior = {}) { + const snapshot = providerSnapshot(); + const nativeAdapter = { + plan: async () => ({ artifact: 'native-plan' }), + reconcileWork: async () => ({ safe: true, reasonCode: 'fixture-reconciled' }), + work: async () => { + f.nativeWorkCalls += 1; + const worktree = path.join(f.worktreeRoot, RUN_ID); + if (!fs.existsSync(worktree)) run('git', ['worktree', 'add', '-b', `coding/${RUN_ID}`, worktree, 'HEAD'], f.repository); + return { artifact: 'native-work' }; + }, + }; + const managedWorkflow = { + providerSnapshot: snapshot, + providerAdapter: { + plan: async (invocation) => providerReceipt(invocation, 'plan'), + // A deterministic provider-boundary outage forces the tested public + // native fallback while preserving the run identity. + work: async () => { throw new Error('fixture provider unavailable'); }, + compound: async (invocation) => { f.learningCalls += 1; return providerReceipt(invocation, 'compound'); }, + }, + }; + const runtime = coding.createCodexRuntime({ registryPath: f.registryPath, nativeAdapter, managedWorkflow }); + const context = runtime.resolveRequest({ repositoryId: f.repositoryId, subjectKey: SUBJECT, workRunId: RUN_ID }); + const workflow = context.managedWorkflow; + // The public MCP accepts no model-controlled verification inputs. The test + // harness supplies bounded, public adapter doubles behind that boundary. + return { + ...runtime, + resolveRequest(input) { + const resolved = runtime.resolveRequest(input); + const bound = (method) => (value) => workflow[method]({ ...value, canonicalWorktree: resolved.canonicalWorktree }); + return { + ...resolved, + managedWorkflow: { + ...workflow, + plan: bound('plan'), acceptPlan: bound('acceptPlan'), work: bound('work'), + status: bound('status'), resume: bound('resume'), + finish: (finishInput) => { + const adapters = successfulAdapters(); + if (behavior.deferredFinish) adapters.tracker.verifyAndClose = async () => ({ status: 'deferred', ok: true }); + return workflow.finish({ ...finishInput, canonicalWorktree: resolved.canonicalWorktree, nonRoutine: true }, adapters); + }, + }, + }; + }, + }; +} + +async function rpc(message, options) { + const output = []; + const original = process.stdout.write; + process.stdout.write = (line) => { output.push(JSON.parse(line)); return true; }; + try { await handle(message, options); } finally { process.stdout.write = original; } + assert.equal(output.length, 1); + return output[0]; +} +async function tool(f, name, argumentsValue, behavior = {}) { + const response = await rpc({ jsonrpc: '2.0', id: name, method: 'tools/call', params: { name, arguments: argumentsValue } }, { registryPath: f.registryPath, createRuntime: () => runtimeFor(f, behavior) }); + assert.ok(response.result, response.error?.message); + return JSON.parse(response.result.content[0].text); +} + +function receiptValidation(receipt, revision) { + const requiredOperations = ['initialize', 'tools/list', 'plan', 'accept-plan', 'work', 'finish', 'status', 'resume']; + const missing = requiredOperations.filter((operation) => !receipt.operations.includes(operation)); + if (receipt.schemaVersion !== 'jarvos-codex-coding-lifecycle-conformance/v1') return 'schemaVersion is invalid'; + if (receipt.jarvosRevision !== revision) return 'receipt revision is stale'; + if (missing.length) return `missing operations: ${missing.join(', ')}`; + if (receipt.restart?.sameRun !== true || receipt.restart?.sameWorktree !== true) return 'restart proof is incomplete'; + if (receipt.verification?.authoritative !== true || receipt.finalizer?.automatic !== true) return 'status-only completion is not accepted'; + return null; +} + +test('clean-profile public MCP lifecycle is deterministic, gated, recoverable, and public-safe', async () => { + const f = fixture(); + try { + const options = { registryPath: f.registryPath, createRuntime: () => runtimeFor(f) }; + const initialized = await rpc({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }, options); + const listed = await rpc({ jsonrpc: '2.0', id: 2, method: 'tools/list' }, options); + assert.equal(initialized.result.serverInfo.name, 'jarvos-coding'); + assert.ok(listed.result.tools.some((entry) => entry.name === 'jarvos_coding_finish')); + + const base = { repositoryId: f.repositoryId, subjectKey: SUBJECT, workRunId: RUN_ID }; + const planned = await tool(f, 'jarvos_coding_plan', { ...base, input: { kind: 'fixture', digest: DIGEST } }); + assert.equal(planned.workRunId, RUN_ID); + const denied = await tool(f, 'jarvos_coding_accept_plan', { ...base, planDigest: DIGEST, packet }); + assert.equal(denied.status, 'awaiting-plan-acceptance'); + + coding.recordOwnerAction({ registryPath: f.registryPath, repositoryId: f.repositoryId, action: 'accept-plan', runId: RUN_ID, revision: DIGEST }); + const accepted = await tool(f, 'jarvos_coding_accept_plan', { ...base, planDigest: DIGEST, packet, artifact: { reference: 'artifact:plan_fixture_001' } }); + assert.equal(accepted.ok, true); + const worked = await tool(f, 'jarvos_coding_work', { ...base, planDigest: DIGEST, packet }); + assert.equal(worked.route, 'native-fallback'); + assert.equal(worked.workRunId, RUN_ID); + assert.equal(f.nativeWorkCalls, 1); + + const finished = await tool(f, 'jarvos_coding_finish', { ...base, planDigest: DIGEST }); + assert.equal(finished.primaryCompletion, 'completed'); + assert.equal(finished.verification.status, 'completed'); + assert.equal(finished.learning.learningStatus, 'captured'); + assert.equal(f.learningCalls, 1); + + const resumed = await tool(f, 'jarvos_coding_resume', base); + const status = await tool(f, 'jarvos_coding_status', base); + assert.equal(resumed.workRunId, RUN_ID); + assert.equal(status.primaryCompletion, 'completed'); + assert.equal(f.learningCalls, 1, 'restart/resume must not publish learning twice'); + assert.equal(f.nativeWorkCalls, 1, 'restart must not create a duplicate native worktree'); + assert.equal(run('git', ['worktree', 'list', '--porcelain'], f.repository).match(/^worktree /gm).length, 2); + assert.match(run('git', ['branch', '--list', `coding/${RUN_ID}`], f.repository), new RegExp(`^[*+] coding/${RUN_ID}$`)); + + const publicState = JSON.stringify(status); + assert.doesNotMatch(publicState, new RegExp(f.base.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); + assert.doesNotMatch(publicState, /credential|secret|token/i); + } finally { fs.rmSync(f.base, { recursive: true, force: true }); } +}); + +test('receipt contract fails closed for stale, incomplete, and status-only claims', () => { + const receipt = require(path.join(ROOT, 'runtimes/codex/coding-lifecycle-conformance.json')); + assert.equal(receiptValidation(receipt, receipt.jarvosRevision), null); + assert.match(receiptValidation({ ...receipt, jarvosRevision: '0'.repeat(40) }, receipt.jarvosRevision), /stale/); + assert.match(receiptValidation({ ...receipt, operations: ['plan'] }, receipt.jarvosRevision), /missing operations/); + assert.match(receiptValidation({ ...receipt, verification: { authoritative: false }, finalizer: { automatic: true } }, receipt.jarvosRevision), /status-only/); + assert.doesNotMatch(JSON.stringify(receipt), /\/Users\/|clawd|Bearer\s|api[_-]?key|token\s*[:=]|secret\s*[:=]/i); +}); + +test('incomplete public finish does not publish a learning', async () => { + const f = fixture(); + try { + const base = { repositoryId: f.repositoryId, subjectKey: SUBJECT, workRunId: RUN_ID }; + await tool(f, 'jarvos_coding_plan', { ...base, input: { kind: 'fixture', digest: DIGEST } }); + coding.recordOwnerAction({ registryPath: f.registryPath, repositoryId: f.repositoryId, action: 'accept-plan', runId: RUN_ID, revision: DIGEST }); + await tool(f, 'jarvos_coding_accept_plan', { ...base, planDigest: DIGEST, packet, artifact: { reference: 'artifact:plan_fixture_001' } }); + const finished = await tool(f, 'jarvos_coding_finish', { ...base, planDigest: DIGEST }, { deferredFinish: true }); + assert.equal(finished.primaryCompletion, 'deferred'); + assert.equal(finished.learning.status, 'not-eligible'); + assert.equal(f.learningCalls, 0); + } finally { fs.rmSync(f.base, { recursive: true, force: true }); } +}); diff --git a/runtimes/codex/coding-lifecycle-conformance.json b/runtimes/codex/coding-lifecycle-conformance.json new file mode 100644 index 00000000..85224ec3 --- /dev/null +++ b/runtimes/codex/coding-lifecycle-conformance.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": "jarvos-codex-coding-lifecycle-conformance/v1", + "jarvosRevision": "4887f9cee38227d5984fbcf9db348368be1d82ea", + "sourceRevisionStrategy": "source-parent", + "status": "passed", + "profileBoundary": "disposable CODEX_HOME", + "mcp": { + "surface": "public JSON-RPC", + "schemaVersion": "2024-11-05", + "directInvocation": true + }, + "provider": { + "boundary": "deterministic public adapter double", + "networkObserved": false + }, + "operations": ["initialize", "tools/list", "plan", "accept-plan", "work", "finish", "status", "resume"], + "restart": { + "sameRun": true, + "sameWorktree": true, + "nativeFallback": true, + "singleBranchAndWorktree": true + }, + "verification": { + "authoritative": true, + "evidenceClass": "live-stage adapter evidence", + "acceptedPlanRequired": true + }, + "learning": { + "eligibleVerifiedCompletion": "captured", + "negativeOrIncomplete": "not-published", + "atMostOnce": true + }, + "finalizer": { + "automatic": true, + "restartRecovery": "one terminal outcome" + } +} diff --git a/tests/pack-manifest-test.js b/tests/pack-manifest-test.js index 5c7cf612..e3de3d54 100644 --- a/tests/pack-manifest-test.js +++ b/tests/pack-manifest-test.js @@ -51,6 +51,7 @@ function advertisedRuntimeAssets() { 'modules/jarvos-skills/src/reconciliation.js', 'modules/jarvos-skills/scripts/dogfood-skills.js', 'modules/jarvos-coding/scripts/jarvos-coding-mcp.js', + 'runtimes/codex/coding-lifecycle-conformance.json', 'modules/jarvos-control-plane/scripts/jarvos-manager.js', 'scripts/release-readiness-check.js', 'scripts/release-status.js', @@ -109,3 +110,16 @@ test('managed provider package and public docs agree on pin, fallback, and admis assert.ok(docs.some((text) => /conformance-backed|healthy.*conformance|conformance.*healthy/i.test(text)), 'public docs must preserve conformance-backed health truth'); assert.ok(docs.some((text) => /plan.*work.*complete/i.test(text)), 'public docs must lead with jarvOS verbs'); }); + +test('packaged coding lifecycle receipt is public-safe and records the deterministic boundary', () => { + const receipt = JSON.parse(fs.readFileSync(path.join(ROOT, 'runtimes/codex/coding-lifecycle-conformance.json'), 'utf8')); + assert.equal(receipt.schemaVersion, 'jarvos-codex-coding-lifecycle-conformance/v1'); + assert.equal(receipt.status, 'passed'); + assert.equal(receipt.profileBoundary, 'disposable CODEX_HOME'); + assert.deepEqual(receipt.operations, ['initialize', 'tools/list', 'plan', 'accept-plan', 'work', 'finish', 'status', 'resume']); + assert.equal(receipt.restart.sameRun, true); + assert.equal(receipt.verification.authoritative, true); + assert.equal(receipt.finalizer.automatic, true); + assert.equal(receipt.provider.networkObserved, false); + assert.doesNotMatch(JSON.stringify(receipt), /\/Users\/|clawd|Bearer\s|api[_-]?key|token\s*[:=]|secret\s*[:=]/i); +}); From 8d7d7a632d26508133b5d2cdc96dc9c2bfb0435c Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 16:04:38 -0400 Subject: [PATCH 07/12] feat(coding): wire public managed Codex workflow --- README.md | 22 ++-- lib/jarvos-cli.js | 8 +- modules/README.md | 11 +- modules/jarvos-coding/README.md | 26 ++-- .../scripts/jarvos-coding-mcp.js | 52 +++++++- .../src/adapters/native-workflow.js | 26 +++- .../src/features/work-run-store/index.js | 52 +++++++- .../src/features/workflow/index.js | 123 ++++++++++++++--- modules/jarvos-coding/src/runtime/codex.js | 1 + .../src/runtime/repository-registry.js | 49 +++++-- .../jarvos-coding/test/codex-runtime.test.js | 12 +- .../test/coding-mcp-pack.test.js | 38 ++++++ modules/jarvos-coding/test/coding-mcp.test.js | 17 ++- .../test/managed-workflow.test.js | 67 +++++++++- .../test/repository-provisioning.test.js | 18 ++- .../jarvos-coding/test/work-run-store.test.js | 16 +++ .../test/codex-coding-conformance.test.js | 55 +++++++- modules/jarvos-skills/README.md | 16 ++- runtimes/codex/README.md | 14 +- runtimes/codex/adapter.json | 1 + .../codex/coding-conformance-prompts.json | 124 ++++++++++++++++++ .../codex/coding-routing-conformance.json | 25 ++++ scripts/release-readiness-check.js | 87 ++++++++++++ tests/release-readiness-check-test.js | 26 +++- 24 files changed, 802 insertions(+), 84 deletions(-) create mode 100644 runtimes/codex/coding-conformance-prompts.json create mode 100644 runtimes/codex/coding-routing-conformance.json diff --git a/README.md b/README.md index 2e94008e..dc4fbcdb 100644 --- a/README.md +++ b/README.md @@ -55,15 +55,19 @@ required to run the stewardship pipeline. sane defaults, route context, define workflows, and make the system feel like one assistant instead of a pile of software. -For coding work, jarvOS-coding bundles a managed Compound Engineering provider -behind the ordinary jarvOS verbs. Say `plan`, `work`, or `complete`; a healthy, -approved provider supplies the stronger planning and execution loop while -jarvOS keeps ownership of the work run, branch, review evidence, submission -gate, and completion decision. Provider learning is a separate, post-verification -tail: `compound` may capture one reusable lesson, but a skipped or failed lesson -never changes a verified coding result. If the provider is missing, modified, -unsupported, or unavailable, jarvOS continues through its native workflow in -the same run and worktree. +For coding work, jarvOS-coding provides a managed run through the public +`plan → accept-plan → work → finish/status/resume` entry. Direct invocation is +the deterministic path: jarvOS owns the work run, branch, review evidence, +submission gate, and completion decision. Automatic learning is a separate +post-verification tail and applies only to verified jarvOS-managed runs in +configured repositories; it never changes a verified coding result. + +Natural routing is currently unavailable: the installed skill is not yet backed +by a current authenticated Codex routing receipt. It must not be assumed to +intercept arbitrary Codex edits. Use the direct managed entry until a live +conformance receipt meets the committed selection and zero-false-claim gates. +If the provider is unavailable, the native jarvOS fallback continues in the +same run and same worktree. Provider installation is managed software, not a JavaScript dependency or a moving upstream branch. jarvOS ships one reviewed pin, preserves unrelated diff --git a/lib/jarvos-cli.js b/lib/jarvos-cli.js index 0863aa33..77040079 100644 --- a/lib/jarvos-cli.js +++ b/lib/jarvos-cli.js @@ -558,7 +558,7 @@ Usage: jarvos coding repository inspect --registry /absolute/registry.json [--json] jarvos coding repository update --registry /absolute/registry.json --repository-id ID --repository-json '{...}' [--json] jarvos coding repository revoke --registry /absolute/registry.json --repository-id ID [--json] - jarvos coding accept --registry /absolute/registry.json --repository-id ID --run-id ID --revision DIGEST [--json] + jarvos coding accept --registry /absolute/registry.json --repository-id ID --run-id ID --revision DIGEST --packet-digest DIGEST [--json] jarvos coding learning decline --registry /absolute/registry.json --repository-id ID --run-id ID [--json] jarvos coding learning reset-retry --registry /absolute/registry.json --repository-id ID --run-id ID [--json] @@ -574,9 +574,9 @@ function parseCodingArgs(argv = []) { const value = argv[index]; if (value === '--help' || value === '-h') { result.help = true; continue; } if (value === '--json') { result.json = true; continue; } - const match = value.match(/^--(registry|repository-id|repository-json|run-id|revision)=(.*)$/); + const match = value.match(/^--(registry|repository-id|repository-json|run-id|revision|packet-digest)=(.*)$/); if (match) { result.options[match[1]] = match[2]; continue; } - if (['--registry', '--repository-id', '--repository-json', '--run-id', '--revision'].includes(value)) { + if (['--registry', '--repository-id', '--repository-json', '--run-id', '--revision', '--packet-digest'].includes(value)) { if (!argv[index + 1]) throw new Error(`${value} requires a value`); result.options[value.slice(2)] = argv[++index]; continue; } @@ -609,7 +609,7 @@ function runCoding(argv = []) { else if (operation === 'revoke') receipt = revokeProvisionedRepository({ registryPath: options.registry, repositoryId: options['repository-id'] }); else throw new Error('repository operation must be add, inspect, update, or revoke'); } else if (area === 'accept' && operation === undefined) { - receipt = recordOwnerAction({ registryPath: options.registry, repositoryId: options['repository-id'], action: 'accept-plan', runId: options['run-id'], revision: options.revision }); + receipt = recordOwnerAction({ registryPath: options.registry, repositoryId: options['repository-id'], action: 'accept-plan', runId: options['run-id'], revision: options.revision, packetDigest: options['packet-digest'] }); } else if (area === 'learning' && operation === 'decline') { receipt = recordOwnerAction({ registryPath: options.registry, repositoryId: options['repository-id'], action: 'decline-learning', runId: options['run-id'] }); } else if (area === 'learning' && operation === 'reset-retry') { diff --git a/modules/README.md b/modules/README.md index 390fe8d1..a8a1ead0 100644 --- a/modules/README.md +++ b/modules/README.md @@ -282,9 +282,14 @@ const { } = require('@jarvos/coding'); ``` -Both adapters register the `jarvos_coding_take_issue_to_done` MCP-style tool and -a `jarvos-coding` skill descriptor when the host supplies a registry. Both call -the same `runTakeIssueToDone` orchestrator. +Both adapters can register the `jarvos_coding_take_issue_to_done` compatibility +tool and a `jarvos-coding` skill descriptor when the host supplies a registry. +Codex's managed public entry is the lifecycle +`plan → accept-plan → work → finish/status/resume`; direct invocation is the +deterministic access path. Natural routing is currently unavailable, so no +global skill installation claims to intercept arbitrary edits or create a +managed run automatically. Automatic learning applies only after a verified +managed run in a configured repository. --- diff --git a/modules/jarvos-coding/README.md b/modules/jarvos-coding/README.md index 8df30b7c..aad4f75a 100644 --- a/modules/jarvos-coding/README.md +++ b/modules/jarvos-coding/README.md @@ -135,14 +135,14 @@ await codex.runTakeIssueToDone({ issueIdentifier: 'SUP-2214' }); ## Managed coding workflow -`createManagedCodingWorkflow(...)` is the provider-neutral route for natural -coding verbs. It owns one durable work run and resolves the approved -Compound Engineering manifest before invoking a provider adapter: +`createManagedCodingWorkflow(...)` is the provider-neutral route behind the +direct managed coding lifecycle. It owns one durable work run and resolves the +approved Compound Engineering manifest before invoking a provider adapter: ```text -plan -> validate draft -> accept one implementation packet -> work -> complete - | - verified learning -> compound (optional) +plan -> validate draft -> accept one implementation packet -> work -> finish + | + status/resume -> verified learning -> compound (optional) ``` The provider is a managed external artifact pinned by @@ -158,10 +158,16 @@ reattachment hints. The Codex adapter is the first conformance-backed CE route. The runtime accepts CE 3.21.4 only when the discovered installation matches the approved immutable pin and the reviewed disposable-profile receipt. Run `jarvos doctor` to see the -approved and discovered versions. If the provider is absent, modified, -disabled, or unavailable, `plan`, `work`, and `complete` fall back to the native -jarvOS route in the same work run and worktree. A failed fallback does not make -a second branch, plan, pull request, or completion claim. +approved and discovered versions. Use `plan → accept-plan → work → +finish/status/resume` through the public MCP lifecycle; if the provider is +absent, modified, disabled, or unavailable, the same managed run may use the +native route after reconciliation. A failed fallback does not make a second +branch, plan, pull request, or completion claim. + +Natural routing is currently unavailable. Deterministic behavior starts at the +direct coding invocation, and automatic learning applies only after a verified +managed run in a configured repository. Arbitrary unmanaged Codex edits do not +receive automatic learning capture. Learning capture is deliberately independent of coding completion. It runs only after live review, tests, pull-request, post-merge, and close evidence establish diff --git a/modules/jarvos-coding/scripts/jarvos-coding-mcp.js b/modules/jarvos-coding/scripts/jarvos-coding-mcp.js index 48aa6ace..850220fe 100755 --- a/modules/jarvos-coding/scripts/jarvos-coding-mcp.js +++ b/modules/jarvos-coding/scripts/jarvos-coding-mcp.js @@ -5,11 +5,12 @@ // intentionally limited to a private registry binding; no filesystem roots, // provider selection, executables, or credentials cross this protocol. const readline = require('node:readline'); +const fs = require('node:fs'); // Import only the public runtime boundary, rather than the package barrel: // MCP initialization must work from an unpacked tarball before any repository // registry is configured. const { createCodexRuntime } = require('../src/runtime/codex'); -const { resolveOwnerPlanAcceptance } = require('../src/runtime/repository-registry'); +const { resolveOwnerPlanAcceptance, resolveOwnerLearningAction } = require('../src/runtime/repository-registry'); const REGISTRY_ENV = 'JARVOS_CODING_REPOSITORY_REGISTRY'; const SHA256 = /^[a-f0-9]{64}$/i; @@ -18,6 +19,7 @@ const SUBJECT = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/; const SAFE_TEXT = /^[^\0\r\n]{1,500}$/; const PATHISH = /(?:^|[\s"'])\/(?:[^\s"']*)/; const SECRET = /(?:\bBearer\s+|\bsk-[A-Za-z0-9_-]{8,}|\bxox[baprs]-|(?:api[_-]?key|token|secret|password)\s*[:=])/i; +const runtimeCache = new Map(); const TOOLS = [ ['jarvos_coding_plan', 'Create or route a managed plan for an owner-provisioned repository subject.'], @@ -56,7 +58,26 @@ function identity(args) { object(args, 'arguments'); known(args, new Set(['repos function packet(value, planDigest) { object(value, 'packet'); known(value, new Set(['version', 'planDigest', 'steps', 'summary']), 'packet'); if (value.version !== 'jarvos-implementation-packet/v1') fail('packet.version is invalid'); digest(value.planDigest, 'packet.planDigest'); if (value.planDigest !== planDigest) fail('packet.planDigest must match planDigest'); if (!Array.isArray(value.steps) || value.steps.length < 1 || value.steps.length > 128) fail('packet.steps must contain 1 to 128 steps'); for (const [i, step] of value.steps.entries()) { object(step, `packet.steps[${i}]`); known(step, new Set(['id', 'description', 'files', 'mutation']), `packet.steps[${i}]`); if (!OPAQUE.test(step.id || '')) fail(`packet.steps[${i}].id is invalid`); safeString(step.description, `packet.steps[${i}].description`); if (step.mutation !== undefined) safeString(step.mutation, `packet.steps[${i}].mutation`); if (step.files !== undefined && (!Array.isArray(step.files) || step.files.some((file) => typeof file !== 'string' || file.startsWith('/') || file.includes('..') || !/^[A-Za-z0-9._/-]+$/.test(file)))) fail(`packet.steps[${i}].files is invalid`); } if (value.summary !== undefined) safeString(value.summary, 'packet.summary'); return value; } function publicValue(value) { if (value == null || typeof value === 'boolean' || typeof value === 'number') return value; if (typeof value === 'string') return (value.length <= 1000 && !PATHISH.test(value) && !SECRET.test(value)) ? value : '[redacted]'; if (Array.isArray(value)) return value.map(publicValue); if (typeof value === 'object') { const output = {}; for (const [key, entry] of Object.entries(value)) if (!/^(?:root|path|worktree|credential|provider|command|executable|detail)$/i.test(key)) output[key] = publicValue(entry); return output; } return null; } function textResult(result, isError = false) { return { content: [{ type: 'text', text: JSON.stringify(publicValue(result)) }], isError }; } -function hostRuntime(options = {}) { const registryPath = options.registryPath || process.env[REGISTRY_ENV]; if (typeof registryPath !== 'string' || !registryPath) fail('coding MCP host registry binding is not configured', -32000); return (options.createRuntime || createCodexRuntime)({ registryPath, ...(options.runtimeOptions || {}) }); } +function hostRuntime(options = {}) { + const registryPath = options.registryPath || process.env[REGISTRY_ENV]; + if (typeof registryPath !== 'string' || !registryPath) fail('coding MCP host registry binding is not configured', -32000); + const factory = options.createRuntime || createCodexRuntime; + // Test and embedding callers may inject a factory/options object whose + // lifetime they own. The packaged MCP process uses the default factory and + // retains one authority-bound runtime so in-flight guards are shared across + // concurrent JSON-RPC calls. Registry metadata changes invalidate it. + if (options.createRuntime || options.runtimeOptions || options.ownerUid !== undefined) { + return factory({ registryPath, ...(options.runtimeOptions || {}) }); + } + let stat; + try { stat = fs.statSync(registryPath); } catch { return factory({ registryPath }); } + const signature = [stat.dev, stat.ino, stat.mtimeMs, stat.ctimeMs, stat.size].join(':'); + const cached = runtimeCache.get(registryPath); + if (cached && cached.signature === signature) return cached.runtime; + const runtime = factory({ registryPath }); + runtimeCache.set(registryPath, { signature, runtime }); + return runtime; +} async function callTool(name, args = {}, options = {}) { if (name === 'jarvos_coding_repositories') { object(args, 'arguments'); known(args, new Set(), 'arguments'); return textResult({ ok: true, repositories: hostRuntime(options).listRepositories() }); } @@ -76,21 +97,42 @@ async function callTool(name, args = {}, options = {}) { // the original opaque tracker identifier as its bounded work reference. const workflowInput = { ...input, subjectKey: context.subjectKey || input.subjectKey, issueIdentifier: args.subjectKey }; if (name === 'jarvos_coding_plan') { if (args.input !== undefined) { object(args.input, 'input'); known(args.input, new Set(['kind', 'digest']), 'input'); safeString(args.input.kind, 'input.kind', 80); digest(args.input.digest, 'input.digest'); } if (args.operationNonce !== undefined) safeString(args.operationNonce, 'operationNonce', 128); return textResult(await workflow.plan({ ...workflowInput, input: args.input, operationNonce: args.operationNonce })); } - if (name === 'jarvos_coding_status' || name === 'jarvos_coding_resume') return textResult(await workflow[name === 'jarvos_coding_status' ? 'status' : 'resume'](workflowInput)); + if (name === 'jarvos_coding_status' || name === 'jarvos_coding_resume') { + let resetLearningRetry = false; + if (!options.createRuntime && !options.resolveOwnerLearningAction) { + resetLearningRetry = Boolean(resolveOwnerLearningAction({ registryPath: options.registryPath || process.env[REGISTRY_ENV], repositoryId: context.repositoryId, runId: context.workRunId, action: 'reset-learning-retry' })); + } else if (options.resolveOwnerLearningAction) { + resetLearningRetry = Boolean(options.resolveOwnerLearningAction({ registryPath: options.registryPath || process.env[REGISTRY_ENV], repositoryId: context.repositoryId, runId: context.workRunId, action: 'reset-learning-retry' })); + } + return textResult(await workflow[name === 'jarvos_coding_status' ? 'status' : 'resume']({ ...workflowInput, resetLearningRetry })); + } const planDigest = digest(args.planDigest, 'planDigest'); - if (name === 'jarvos_coding_finish') return textResult(await workflow.finish({ ...workflowInput, planDigest })); + if (name === 'jarvos_coding_finish') { + let declineLearning = false; + if (!options.createRuntime && !options.resolveOwnerLearningAction) { + declineLearning = Boolean(resolveOwnerLearningAction({ registryPath: options.registryPath || process.env[REGISTRY_ENV], repositoryId: context.repositoryId, runId: context.workRunId, action: 'decline-learning' })); + } else if (options.resolveOwnerLearningAction) { + declineLearning = Boolean(options.resolveOwnerLearningAction({ registryPath: options.registryPath || process.env[REGISTRY_ENV], repositoryId: context.repositoryId, runId: context.workRunId, action: 'decline-learning' })); + } + return textResult(await workflow.finish({ ...workflowInput, planDigest, declineLearning })); + } const implementationPacket = packet(args.packet, planDigest); if (args.operationNonce !== undefined) safeString(args.operationNonce, 'operationNonce', 128); if (name === 'jarvos_coding_accept_plan') { if (args.expectedPlanDigest !== undefined && args.expectedPlanDigest !== null) digest(args.expectedPlanDigest, 'expectedPlanDigest'); if (args.artifact !== undefined) { object(args.artifact, 'artifact'); known(args.artifact, new Set(['reference']), 'artifact'); if (typeof args.artifact.reference !== 'string' || !/^artifact:[A-Za-z0-9._-]{6,160}$/.test(args.artifact.reference)) fail('artifact.reference is invalid'); } let acceptanceEvidence = null; - if (context.repository.acceptancePolicy.mode !== 'agent-mediated-allowed') acceptanceEvidence = (options.resolveOwnerAcceptance || resolveOwnerPlanAcceptance)({ registryPath: options.registryPath || process.env[REGISTRY_ENV], repositoryId: context.repositoryId, runId: context.workRunId, planDigest, ...(options.ownerUid === undefined ? {} : { ownerUid: options.ownerUid }) }); + if (context.repository.acceptancePolicy.mode !== 'agent-mediated-allowed') acceptanceEvidence = (options.resolveOwnerAcceptance || resolveOwnerPlanAcceptance)({ registryPath: options.registryPath || process.env[REGISTRY_ENV], repositoryId: context.repositoryId, runId: context.workRunId, planDigest, packetDigest: sha256Json(implementationPacket), ...(options.ownerUid === undefined ? {} : { ownerUid: options.ownerUid }) }); if (context.repository.acceptancePolicy.mode !== 'agent-mediated-allowed' && !acceptanceEvidence) return textResult({ ok: false, status: 'awaiting-plan-acceptance', workRunId: context.workRunId, reasonCode: 'owner_acceptance_required' }, true); return textResult(await workflow.acceptPlan({ ...workflowInput, planDigest, packet: implementationPacket, expectedPlanDigest: args.expectedPlanDigest, artifact: args.artifact, operationNonce: args.operationNonce, acceptanceEvidence })); } return textResult(await workflow.work({ ...workflowInput, planDigest, packet: implementationPacket, operationNonce: args.operationNonce })); } +function sha256Json(value) { + const crypto = require('node:crypto'); + return crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex'); +} + function write(message) { process.stdout.write(`${JSON.stringify(message)}\n`); } async function handle(message, options = {}) { if (!message || typeof message !== 'object') return; const { id, method, params } = message; if (!id && String(method || '').startsWith('notifications/')) return; try { if (method === 'initialize') return write({ jsonrpc: '2.0', id, result: { protocolVersion: params?.protocolVersion || '2024-11-05', capabilities: { tools: {} }, serverInfo: { name: 'jarvos-coding', version: '0.1.0' } } }); if (method === 'tools/list') return write({ jsonrpc: '2.0', id, result: { tools: TOOLS } }); if (method === 'tools/call') return write({ jsonrpc: '2.0', id, result: await callTool(params?.name, params?.arguments === undefined ? {} : params.arguments, options) }); write({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } }); } catch (error) { write({ jsonrpc: '2.0', id, error: { code: error.code || -32000, message: publicValue(error.message || String(error)) } }); } } function main() { const rl = readline.createInterface({ input: process.stdin }); rl.on('line', (line) => { if (!line.trim()) return; try { handle(JSON.parse(line)); } catch (error) { write({ jsonrpc: '2.0', error: { code: -32700, message: 'Parse error' } }); } }); } diff --git a/modules/jarvos-coding/src/adapters/native-workflow.js b/modules/jarvos-coding/src/adapters/native-workflow.js index d075082e..49a01033 100644 --- a/modules/jarvos-coding/src/adapters/native-workflow.js +++ b/modules/jarvos-coding/src/adapters/native-workflow.js @@ -3,6 +3,7 @@ const { runTakeIssueToDone } = require('../features/orchestrator'); const NATIVE_WORKFLOW_SCHEMA_VERSION = 'jarvos-native-workflow/v1'; +const NATIVE_PLAN_SCHEMA_VERSION = 'jarvos-native-plan/v1'; function createNativeWorkflowAdapter(options = {}) { const execute = options.runTakeIssueToDone || runTakeIssueToDone; @@ -10,12 +11,31 @@ function createNativeWorkflowAdapter(options = {}) { return Object.freeze({ schemaVersion: NATIVE_WORKFLOW_SCHEMA_VERSION, async plan(invocation) { - if (typeof options.plan !== 'function') throw new Error('public native planning dependency is unavailable'); - return options.plan(invocation); + if (typeof options.plan === 'function') return options.plan(invocation); + // The native route must remain usable when the optional Compound + // Engineering provider is unavailable. This is deliberately a bounded + // planning scaffold: it records the stable task digest and leaves the + // implementation packet to the managed acceptance boundary rather than + // inventing files, commands, or completion evidence. + const planDigest = invocation?.input?.digest; + return { + schemaVersion: NATIVE_PLAN_SCHEMA_VERSION, + planDigest, + summary: 'Implement the requested change in the owner-provisioned repository.', + steps: [{ + id: 'step_01', + description: 'Implement the requested change in the owner-provisioned repository.', + }], + }; }, async work(invocation) { if (typeof options.work === 'function') return options.work(invocation); - return execute({ ...invocation.input, workRunId: invocation.workRunId, canonicalWorktree: invocation.canonicalWorktree }, options.adapters || {}); + return execute({ + ...invocation.input, + issueIdentifier: invocation.issueIdentifier || invocation.input?.issueIdentifier, + workRunId: invocation.workRunId, + canonicalWorktree: invocation.canonicalWorktree, + }, options.adapters || {}); }, async reconcileWork(invocation) { if (typeof options.reconcileWork !== 'function') return { safe: false, reasonCode: 'authoritative_reconciliation_unavailable' }; diff --git a/modules/jarvos-coding/src/features/work-run-store/index.js b/modules/jarvos-coding/src/features/work-run-store/index.js index 770e2a3a..46fde205 100644 --- a/modules/jarvos-coding/src/features/work-run-store/index.js +++ b/modules/jarvos-coding/src/features/work-run-store/index.js @@ -21,6 +21,7 @@ const SUBJECT_KEY = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/; const ABSOLUTE_PATH = /^(?:\/(?!\/)|[A-Za-z]:[\\/]|\\\\)/; const SECRET_VALUE = /(?:\bBearer\s+|\bsk-[A-Za-z0-9_-]{8,}|\bxox[baprs]-|(?:api[_-]?key|token|secret|password)\s*[:=])/i; const AUTHORITY_KEYS = new Set(['branch', 'worktree', 'worktreePath', 'approval', 'submissionReady', 'completion', 'owner', 'authority', 'terminalStatus', 'nextStep', 'pr', 'pullRequest']); +const NATIVE_COMPLETION_MAX_BYTES = 512 * 1024; function clone(value) { return value == null ? value : JSON.parse(JSON.stringify(value)); @@ -81,6 +82,10 @@ function validateState(state) { if (!Array.isArray(run.events)) errors.push(`workRuns.${id}.events must be an array`); if (!Array.isArray(run.artifacts)) errors.push(`workRuns.${id}.artifacts must be an array`); if (!isObject(run.recovery)) errors.push(`workRuns.${id}.recovery must be an object`); + if (run.nativeCompletion !== undefined && run.nativeCompletion !== null) { + if (!isObject(run.nativeCompletion) || !isObject(run.nativeCompletion.result) || !isObject(run.nativeCompletion.evidence)) errors.push(`workRuns.${id}.nativeCompletion is invalid`); + else if (JSON.stringify(run.nativeCompletion).length > NATIVE_COMPLETION_MAX_BYTES) errors.push(`workRuns.${id}.nativeCompletion is too large`); + } } return { ok: errors.length === 0, errors }; } @@ -251,6 +256,7 @@ function createWorkRunStore(options = {}) { eventNonces: {}, recovery: { state: 'active', reasonCode: null, updatedAt: now }, terminalEvidence: null, + nativeCompletion: null, learningTail: { status: 'not-evaluated', attempts: 0, signal: null, reasonCode: null, artifact: null, updatedAt: now }, createdAt: now, updatedAt: now, @@ -380,7 +386,11 @@ function createWorkRunStore(options = {}) { const current = run.acceptedPlan?.digest || null; if (current !== expected) return noCommit({ ok: false, reason: 'plan_compare_and_set_conflict', authoritativePlanDigest: current, public: publicRun(run) }); if (input.packetDigest !== undefined && !isDigest(input.packetDigest)) return noCommit({ ok: false, reason: 'invalid_packet_digest' }); - const artifact = normalizeArtifact({ ...(input.artifact || {}), kind: 'plan', digest: input.planDigest }, { allowPrivatePath: true }); + const artifact = normalizeArtifact({ + ...(input.artifact || { reference: `artifact:plan_${input.planDigest.slice(0, 24)}` }), + kind: 'plan', + digest: input.planDigest, + }, { allowPrivatePath: true }); if (run.providerSnapshot && input.providerPinDigest && run.providerSnapshot.pinDigest !== input.providerPinDigest) return noCommit({ ok: false, reason: 'provider_pin_conflict' }); run.acceptedPlan = { digest: input.planDigest, @@ -434,6 +444,25 @@ function createWorkRunStore(options = {}) { }); } + function setNativeCompletion(input = {}) { + return mutate((state) => { + const run = state.workRuns[input.workRunId]; + if (!run) return noCommit({ ok: false, reason: 'not_found' }); + const owner = assertRunOwner(run, input.ownerId, input.fence); + if (!owner.ok) return noCommit(owner); + if (!isObject(input.completion) || !isObject(input.completion.result) || !isObject(input.completion.evidence)) return noCommit({ ok: false, reason: 'invalid_native_completion' }); + if (JSON.stringify(input.completion).length > NATIVE_COMPLETION_MAX_BYTES) return noCommit({ ok: false, reason: 'native_completion_too_large' }); + if (run.nativeCompletion) { + const same = digest(run.nativeCompletion.result) === digest(input.completion.result) + && digest(run.nativeCompletion.evidence) === digest(input.completion.evidence); + return same ? noCommit({ ok: true, deduped: true, nativeCompletion: clone(run.nativeCompletion), workRun: clone(run), public: publicRun(run) }) : noCommit({ ok: false, reason: 'native_completion_conflict' }); + } + run.nativeCompletion = { result: clone(input.completion.result), evidence: clone(input.completion.evidence), recordedAt: nowIso(clock) }; + run.updatedAt = run.nativeCompletion.recordedAt; + return { ok: true, nativeCompletion: clone(run.nativeCompletion), workRun: clone(run), public: publicRun(run) }; + }); + } + function setLearningTail(input = {}) { return mutate((state) => { const run = state.workRuns[input.workRunId]; @@ -492,6 +521,21 @@ function createWorkRunStore(options = {}) { }); } + function resetLearningFinalizer(input = {}) { + return mutate((state) => { + const run = state.workRuns[input.workRunId]; + if (!run) return noCommit({ ok: false, reason: 'not_found' }); + const owner = assertRunOwner(run, input.ownerId, input.fence); + if (!owner.ok) return noCommit(owner); + const current = run.learningTail || { status: 'not-evaluated', attempts: 0, signal: null, artifact: null }; + if (!['unavailable', 'failed', 'unsafe', 'retryable-unavailable', 'finalizing'].includes(current.status)) return noCommit({ ok: true, deduped: true, learningTail: clone(current) }); + const next = { ...current, status: 'retryable-unavailable', attempts: 0, reasonCode: 'owner_retry_reset', updatedAt: nowIso(clock) }; + run.learningTail = next; + run.updatedAt = next.updatedAt; + return { ok: true, learningTail: clone(next), workRun: clone(run), public: publicRun(run) }; + }); + } + function recordProviderReceipt(input = {}) { const validation = validateWorkflowProviderReceipt(input.receipt, { manifest: input.manifest, request: input.request }); if (!validation.ok) return { ok: false, reason: 'invalid_provider_receipt', errors: validation.errors }; @@ -521,9 +565,11 @@ function createWorkRunStore(options = {}) { acceptPlan, setRecoveryState, setTerminalEvidence, + setNativeCompletion, setLearningTail, reserveLearningFinalizer, reconcileLearningFinalizer, + resetLearningFinalizer, projectPublicWorkRun: (workRun) => publicRun(workRun), validateState, }; @@ -565,7 +611,9 @@ function createFileWorkRunStore(rootDir, options = {}) { throw error; } finally { if (fd !== undefined) fs.closeSync(fd); - try { fs.unlinkSync(lockPath); } catch (error) { if (error.code !== 'ENOENT') throw error; } + if (fd !== undefined) { + try { fs.unlinkSync(lockPath); } catch (error) { if (error.code !== 'ENOENT') throw error; } + } } } const backend = { diff --git a/modules/jarvos-coding/src/features/workflow/index.js b/modules/jarvos-coding/src/features/workflow/index.js index 893451dc..7aa92aef 100644 --- a/modules/jarvos-coding/src/features/workflow/index.js +++ b/modules/jarvos-coding/src/features/workflow/index.js @@ -108,6 +108,7 @@ function createManagedCodingWorkflow(options = {}) { const manifest = resolveManifest(options); const providerAdapter = options.providerAdapter || {}; const nativeAdapter = options.nativeAdapter || {}; + const finishAdapters = options.finishAdapters || {}; const ownerId = options.ownerId || 'jarvos-coding'; const providerSnapshot = options.providerSnapshot || null; const providerSnapshotVerifier = options.providerSnapshotVerifier; @@ -179,6 +180,7 @@ function createManagedCodingWorkflow(options = {}) { idempotencyKey: input.idempotencyKey || `${operation}:${claimed.workRunId}`, provider: null, canonicalWorktree: input.canonicalWorktree || claimed.workRun.canonicalWorktree, + issueIdentifier: input.issueIdentifier || input.issue?.identifier || input.subjectKey, input: clone(requestInput), args: [operation, claimed.workRunId, operationNonce], env: { @@ -210,6 +212,61 @@ function createManagedCodingWorkflow(options = {}) { }); } + function compactStage(value) { + if (!isObject(value)) return null; + const allowed = new Set(['status', 'state', 'ok', 'liveConfirmed', 'reattached', 'alreadyClosed', 'merged', 'url', 'number', 'artifact', 'summary', 'reason', 'reasonCode', 'notApplicable', 'clean', 'baseBranch', 'baseRef', 'intendedFiles', 'tests']); + const result = {}; + for (const key of allowed) { + if (value[key] === undefined) continue; + if (key === 'tests' && Array.isArray(value[key])) result.tests = value[key].slice(0, 64).map((entry) => compactStage(entry)).filter(Boolean); + else if (['artifact', 'summary', 'reason', 'reasonCode', 'url', 'number', 'status', 'state', 'baseBranch', 'baseRef'].includes(key)) { + if (typeof value[key] === 'string' && value[key].length <= 500) result[key] = value[key]; + } else if (key === 'intendedFiles' && Array.isArray(value[key])) result[key] = value[key].filter((entry) => typeof entry === 'string' && entry.length <= 300).slice(0, 128); + else if (typeof value[key] === 'boolean') result[key] = value[key]; + } + return result; + } + + function nativeCompletionProjection(result) { + if (!isObject(result) || result.status !== 'completed') return null; + const assessment = assessTerminalSubmission(result); + if (!assessment.ok) return null; + const evidence = assessment.submissionEvidence; + const events = (Array.isArray(result.events) ? result.events : []).slice(0, 256).map((event) => ({ + stage: typeof event?.stage === 'string' ? event.stage.slice(0, 80) : null, + result: compactStage(event?.result), + })).filter((event) => event.stage && event.result); + const safeEvidence = { + issueIdentifier: typeof evidence.issueIdentifier === 'string' ? evidence.issueIdentifier.slice(0, 255) : null, + branch: typeof evidence.branch === 'string' ? evidence.branch.slice(0, 255) : null, + checkpoint: compactStage(evidence.checkpoint), + git: compactStage(evidence.git), + pullRequest: compactStage(evidence.pullRequest), + postMergeSweep: compactStage(evidence.postMergeSweep), + verifyClose: compactStage(evidence.verifyClose), + events, + }; + return { + result: { + ok: true, + status: 'completed', + issueIdentifier: safeEvidence.issueIdentifier, + branch: safeEvidence.branch, + baseRef: typeof result.baseRef === 'string' ? result.baseRef.slice(0, 255) : null, + intendedFiles: Array.isArray(result.intendedFiles) ? result.intendedFiles.filter((entry) => typeof entry === 'string').slice(0, 128) : [], + checkpoints: Array.isArray(result.checkpoints) ? result.checkpoints.slice(-1).map(compactStage).filter(Boolean) : [], + events, + }, + evidence: safeEvidence, + }; + } + + function persistNativeCompletion(claimed, result) { + const completion = nativeCompletionProjection(result); + if (!completion || typeof options.workRunStore.setNativeCompletion !== 'function') return { ok: true, persisted: false }; + return options.workRunStore.setNativeCompletion({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, completion }); + } + function providerIdentity(snapshot) { if (!isObject(snapshot)) return snapshot; const { status, observedAt, ...identity } = snapshot; @@ -321,14 +378,14 @@ function createManagedCodingWorkflow(options = {}) { if (!reconciled || reconciled.safe !== true) { return { ok: false, status: 'blocked', route: 'native-fallback', workRunId: claimed.workRunId, reasonCode, detail }; } - const resumed = options.workRunStore.setRecoveryState({ + const preparing = options.workRunStore.setRecoveryState({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, - state: 'active', - reasonCode: 'provider_work_reconciled', + state: 'blocked', + reasonCode: 'native_fallback_in_progress', }); - if (!resumed.ok || typeof nativeAdapter.work !== 'function') { + if (!preparing.ok || typeof nativeAdapter.work !== 'function') { return { ok: false, status: 'blocked', route: 'native-fallback', workRunId: claimed.workRunId, reasonCode, detail }; } let native; @@ -344,6 +401,11 @@ function createManagedCodingWorkflow(options = {}) { }); return { ok: false, status: 'blocked', route: 'native-fallback', workRunId: claimed.workRunId, reasonCode: 'native_fallback_failed', detail: error.message }; } + const completion = persistNativeCompletion(claimed, native); + if (!completion.ok) { + options.workRunStore.setRecoveryState({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, state: 'failed', reasonCode: 'native_completion_not_recorded' }); + return { ok: false, status: 'blocked', route: 'native-fallback', workRunId: claimed.workRunId, reasonCode: 'native_completion_not_recorded' }; + } const recoveryEvent = recordNativeRecovery(claimed, invocation); if (!recoveryEvent.ok) { options.workRunStore.setRecoveryState({ @@ -355,6 +417,16 @@ function createManagedCodingWorkflow(options = {}) { }); return { ok: false, status: 'blocked', route: 'native-fallback', workRunId: claimed.workRunId, reasonCode: 'recovery_event_not_recorded' }; } + const completed = options.workRunStore.setRecoveryState({ + workRunId: claimed.workRunId, + ownerId: claimed.ownerId, + fence: claimed.fence, + state: 'active', + reasonCode: 'native_fallback_succeeded', + }); + if (!completed.ok) { + return { ok: false, status: 'blocked', route: 'native-fallback', workRunId: claimed.workRunId, reasonCode: 'recovery_state_not_recorded' }; + } return { ok: true, status: 'succeeded', route: 'native-fallback', workRunId: claimed.workRunId, work: native }; } @@ -413,7 +485,7 @@ function createManagedCodingWorkflow(options = {}) { // runtime always passes the owner-controlled policy explicitly. const policy = input.acceptancePolicy || options.acceptancePolicy || { mode: 'agent-mediated-allowed' }; const evidence = input.acceptanceEvidence || null; - if (policy.mode !== 'agent-mediated-allowed' && (!evidence || evidence.planDigest !== input.planDigest || typeof evidence.source !== 'string')) { + if (policy.mode !== 'agent-mediated-allowed' && (!evidence || evidence.planDigest !== input.planDigest || evidence.packetDigest !== digest(packetValidation.packet) || typeof evidence.source !== 'string')) { return { ok: false, status: 'awaiting-plan-acceptance', workRunId: claimed.workRunId, reasonCode: 'acceptance_evidence_required' }; } return options.workRunStore.acceptPlan({ @@ -519,6 +591,11 @@ function createManagedCodingWorkflow(options = {}) { }); return { ok: false, status: 'blocked', route: 'native-fallback', workRunId: claimed.workRunId, reasonCode: 'native_fallback_failed', detail: error.message }; } + const completion = persistNativeCompletion(claimed, native); + if (!completion.ok) { + options.workRunStore.setRecoveryState({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, state: 'failed', reasonCode: 'native_completion_not_recorded' }); + return { ok: false, status: 'blocked', route: 'native-fallback', workRunId: claimed.workRunId, reasonCode: 'native_completion_not_recorded' }; + } const recorded = recordNativeRecovery(claimed, nativeInvocationValue); if (!recorded.ok) { options.workRunStore.setRecoveryState({ @@ -602,7 +679,7 @@ function createManagedCodingWorkflow(options = {}) { }; } - const eligibility = input.persistLearningSignal === true + const eligibility = input.persistLearningSignal === true && input.declineLearning !== true && input.declinedLearning !== true ? { status: 'eligible', learning: input.learning, deferredCount: 0 } : evaluateLearningEligibility({ verification: input.verification || input.orchestration, @@ -710,24 +787,28 @@ function createManagedCodingWorkflow(options = {}) { }; } - async function complete(input = {}, adapters = {}) { - const claimed = claim(input); + async function runCompletion(claimed, input = {}, adapters = {}) { if (!claimed.workRun.acceptedPlan || claimed.workRun.acceptedPlan.digest !== input.planDigest) { return { ok: false, status: 'awaiting-plan-acceptance', workRunId: claimed.workRunId, reasonCode: 'accepted_plan_mismatch' }; } - const result = await runTakeIssueToDone({ ...input, workRunId: claimed.workRunId, branch: input.branch || input.branchName }, adapters); + if (claimed.workRun.nativeCompletion?.result) return { ...clone(claimed.workRun.nativeCompletion.result), workRunId: claimed.workRunId, route: 'native-fallback', deduped: true }; + const result = await runTakeIssueToDone({ ...input, workRunId: claimed.workRunId, branch: input.branch || input.branchName }, Object.keys(adapters).length ? adapters : finishAdapters); return { ...result, workRunId: claimed.workRunId, route: 'jarvos-orchestrator' }; } + async function complete(input = {}, adapters = {}) { + return runCompletion(claim(input), input, adapters); + } + async function finish(input = {}, adapters = {}) { const claimed = claim(input); const run = claimed.workRun; if (!run.acceptedPlan || run.acceptedPlan.digest !== input.planDigest) { return { ok: false, status: 'awaiting-plan-acceptance', workRunId: claimed.workRunId, reasonCode: 'accepted_plan_mismatch' }; } - const result = await complete(input, adapters); + const result = await runCompletion(claimed, input, adapters); if (result.ok === false) return result; - const assessment = assessTerminalSubmission(result); + const assessment = assessTerminalSubmission(result, { submissionEvidence: run.nativeCompletion?.evidence }); const verification = { ...result, submissionGate: assessment.submissionGate, @@ -746,9 +827,11 @@ function createManagedCodingWorkflow(options = {}) { }, }; } + const terminalPayload = verification.submissionGate || verification.events || verification; + const terminalDigest = digest(JSON.stringify(terminalPayload)); const evidence = { - reference: `terminal_${digest(JSON.stringify(verification.submissionGate || verification.events || verification)).slice(0, 24)}`, - digest: digest(JSON.stringify(verification.submissionGate || verification.events || verification)), + reference: `terminal_${terminalDigest.slice(0, 24)}`, + digest: terminalDigest, status: result.status, }; const terminal = options.workRunStore.setTerminalEvidence({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, evidence }); @@ -758,8 +841,9 @@ function createManagedCodingWorkflow(options = {}) { return { ...result, verification: terminal.ok ? terminal.terminalEvidence : null, primaryCompletion: result.status, learning: { status: derived.status, reasonCode: derived.reasonCode } }; } options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'eligible', signal: derived.signal, reasonCode: 'reusable_learning_signal' }); - const learning = await compound({ ...input, workRunId: claimed.workRunId, verification, learning: derived.signal, persistLearningSignal: true }); + const learning = await compound({ ...input, workRunId: claimed.workRunId, verification, learning: derived.signal, persistLearningSignal: input.declineLearning !== true, declineLearning: input.declineLearning === true }); if (learning.learningStatus === 'captured' || learning.status === 'succeeded') options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'captured', artifact: learning.artifact || null }); + if (learning.learningStatus === 'declined' || learning.status === 'declined') options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'declined', reasonCode: learning.reasonCode || 'owner_declined_learning' }); return { ...result, verification: terminal.ok ? terminal.terminalEvidence : null, primaryCompletion: result.status, learning }; } @@ -768,9 +852,14 @@ function createManagedCodingWorkflow(options = {}) { const run = options.workRunStore.getWorkRun(claimed.workRunId, { public: false }); if (!run?.terminalEvidence) return { ok: true, status: run?.state || 'active', workRunId: claimed.workRunId, learning: null }; const tail = run.learningTail || { status: 'not-evaluated' }; - if (!tail.signal || ['captured', 'not-eligible', 'declined', 'unsafe', 'unavailable'].includes(tail.status)) return { ok: true, status: 'verified', workRunId: claimed.workRunId, primaryCompletion: 'completed', learning: tail }; - if (tail.status === 'finalizing') options.workRunStore.reconcileLearningFinalizer({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence }); - const learning = await compound({ ...input, workRunId: claimed.workRunId, planDigest: run.acceptedPlan?.digest, verification: { status: 'completed', submissionGate: { ready: true }, events: [] }, learning: tail.signal, persistLearningSignal: true }); + if (input.resetLearningRetry === true && typeof options.workRunStore.resetLearningFinalizer === 'function') { + options.workRunStore.resetLearningFinalizer({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence }); + } + const refreshed = options.workRunStore.getWorkRun(claimed.workRunId, { public: false }); + const currentTail = refreshed?.learningTail || tail; + if (!currentTail.signal || ['captured', 'not-eligible', 'declined', 'unsafe', 'unavailable'].includes(currentTail.status)) return { ok: true, status: 'verified', workRunId: claimed.workRunId, primaryCompletion: 'completed', learning: currentTail }; + if (currentTail.status === 'finalizing') options.workRunStore.reconcileLearningFinalizer({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence }); + const learning = await compound({ ...input, workRunId: claimed.workRunId, planDigest: run.acceptedPlan?.digest, verification: { status: 'completed', submissionGate: { ready: true }, events: [] }, learning: currentTail.signal, persistLearningSignal: true }); if (learning.learningStatus === 'captured' || learning.status === 'succeeded') options.workRunStore.setLearningTail({ workRunId: claimed.workRunId, ownerId: claimed.ownerId, fence: claimed.fence, status: 'captured', artifact: learning.artifact || null }); return { ok: true, status: 'verified', workRunId: claimed.workRunId, primaryCompletion: 'completed', learning }; } diff --git a/modules/jarvos-coding/src/runtime/codex.js b/modules/jarvos-coding/src/runtime/codex.js index baa4fd0a..8dc6ab60 100644 --- a/modules/jarvos-coding/src/runtime/codex.js +++ b/modules/jarvos-coding/src/runtime/codex.js @@ -44,6 +44,7 @@ function createCodexRuntime(options = {}) { ...(options.managedWorkflow || {}), workRunStore: store, nativeAdapter, + finishAdapters: options.managedWorkflow?.finishAdapters || liveAdapters, manifestPath: options.managedWorkflow?.manifestPath || path.resolve(__dirname, '../../providers/compound-engineering.json'), acceptancePolicy: repository.acceptancePolicy, ownerId: options.ownerId || 'jarvos-coding', diff --git a/modules/jarvos-coding/src/runtime/repository-registry.js b/modules/jarvos-coding/src/runtime/repository-registry.js index a9bff6bd..65265dd8 100644 --- a/modules/jarvos-coding/src/runtime/repository-registry.js +++ b/modules/jarvos-coding/src/runtime/repository-registry.js @@ -35,19 +35,28 @@ function deriveRepositoryId(entry) { function publicRepository(entry) { return { repositoryId: entry.repositoryId, label: entry.publicLabel, agentSelectable: entry.agentSelectable }; } -function normalizeEntry(raw) { +function assertPrivateDirectory(value, label, ownerUid = process.getuid?.()) { + let stat; + try { stat = fs.lstatSync(value); } catch { throw new Error(`${label} must exist and resolve canonically`); } + if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`${label} must be a real directory`); + if (ownerUid !== undefined && stat.uid !== ownerUid) throw new Error(`${label} is not owned by the expected owner`); + if ((stat.mode & 0o077) !== 0) throw new Error(`${label} permissions must not grant group or other access`); +} +function normalizeEntry(raw, options = {}) { if (!isObject(raw)) throw new Error('repository entry must be an object'); const allowed = new Set(['repositoryId', 'id', 'publicLabel', 'agentSelectable', 'root', 'repositoryRoot', 'stateRoot', 'tracker', 'worktreePolicy', 'acceptancePolicy', 'providerEgressPolicy', 'credentialReferences', 'learning', 'learningPublicationTarget']); for (const key of Object.keys(raw)) if (!allowed.has(key)) throw new Error(`repository entry.${key} is not allowed`); assertNoSecret(raw); const root = canonical(raw.root || raw.repositoryRoot, 'repository root'); const stateRoot = canonical(raw.stateRoot, 'repository stateRoot'); + assertPrivateDirectory(raw.stateRoot, 'repository stateRoot', options.ownerUid); const repositoryId = raw.repositoryId || raw.id || deriveRepositoryId({ ...raw, root }); if (!OPAQUE_ID.test(repositoryId)) throw new Error('repositoryId must be an opaque identifier'); if (typeof raw.publicLabel !== 'string' || !SAFE_LABEL.test(raw.publicLabel) || SECRET.test(raw.publicLabel) || ABSOLUTE_PATH.test(raw.publicLabel)) throw new Error('repository publicLabel must be public-safe text'); if (typeof raw.agentSelectable !== 'boolean') throw new Error('repository agentSelectable must be boolean'); if (!isObject(raw.worktreePolicy)) throw new Error('repository worktreePolicy is required'); const worktreeRoot = canonical(raw.worktreePolicy.root || raw.worktreePolicy.worktreeRoot, 'repository worktreePolicy.root'); + assertPrivateDirectory(raw.worktreePolicy.root || raw.worktreePolicy.worktreeRoot, 'repository worktreePolicy.root', options.ownerUid); if (inside(root, stateRoot) || inside(root, worktreeRoot) || inside(stateRoot, root) || inside(worktreeRoot, root) || inside(stateRoot, worktreeRoot) || inside(worktreeRoot, stateRoot)) throw new Error('repository roots must not overlap'); const acceptancePolicy = raw.acceptancePolicy || { mode: 'human-evidence-required' }; if (!isObject(acceptancePolicy) || !ACCEPTANCE_MODES.has(acceptancePolicy.mode)) throw new Error('repository acceptancePolicy.mode is invalid'); @@ -64,7 +73,7 @@ function normalizeEntry(raw) { learning: Object.freeze({ ...(raw.learning || {}) }), learningPublicationTarget: raw.learningPublicationTarget || null, }); } -function validateRepositoryRegistry(registry) { +function validateRepositoryRegistry(registry, options = {}) { const errors = []; if (!isObject(registry)) return { ok: false, errors: ['registry must be an object'] }; for (const key of Object.keys(registry)) if (!new Set(['schemaVersion', 'generation', 'repositories']).has(key)) errors.push(`registry.${key} is not allowed`); @@ -72,7 +81,7 @@ function validateRepositoryRegistry(registry) { if (!Number.isInteger(registry.generation) || registry.generation < 1) errors.push('registry.generation must be a positive integer'); if (!Array.isArray(registry.repositories)) errors.push('registry.repositories must be an array'); const entries = []; - for (const raw of registry.repositories || []) { try { entries.push(normalizeEntry(raw)); } catch (error) { errors.push(error.message); } } + for (const raw of registry.repositories || []) { try { entries.push(normalizeEntry(raw, options)); } catch (error) { errors.push(error.message); } } const ids = new Set(); for (const entry of entries) { if (ids.has(entry.repositoryId)) errors.push(`duplicate repositoryId ${entry.repositoryId}`); ids.add(entry.repositoryId); } return { ok: errors.length === 0, errors, entries }; @@ -85,7 +94,7 @@ function loadRepositoryRegistry(registryPath, options = {}) { if ((stat.mode & 0o077) !== 0) throw new Error('registryPath permissions must not grant group or other access'); if (options.ownerUid !== undefined && stat.uid !== options.ownerUid) throw new Error('registryPath is not owned by the expected owner'); let parsed; try { parsed = JSON.parse(fs.readFileSync(resolvedPath, 'utf8')); } catch (error) { throw new Error(`registryPath contains invalid JSON: ${error.message}`); } - const validation = validateRepositoryRegistry(parsed); + const validation = validateRepositoryRegistry(parsed, options); if (!validation.ok) throw new Error(`invalid repository registry: ${validation.errors.join('; ')}`); const byId = new Map(validation.entries.map((entry) => [entry.repositoryId, entry])); return Object.freeze({ schemaVersion: REPOSITORY_REGISTRY_SCHEMA_VERSION, generation: parsed.generation, path: resolvedPath, repositories: validation.entries, resolve(repositoryId) { @@ -205,10 +214,11 @@ function revokeProvisionedRepository({ registryPath, repositoryId, ownerUid } = if (!current) throw new Error('unknown repository'); return writeProvisionedRegistry(registryPath, { schemaVersion: REPOSITORY_REGISTRY_SCHEMA_VERSION, generation: existing.generation + 1, repositories: existing.repositories.filter((item) => item.repositoryId !== repositoryId) }, 'revoked', current, { ownerUid }); } -function recordOwnerAction({ registryPath, repositoryId, action, runId, revision, ownerUid, now = new Date() } = {}) { +function recordOwnerAction({ registryPath, repositoryId, action, runId, revision, packetDigest, ownerUid, now = new Date() } = {}) { if (!new Set(['accept-plan', 'decline-learning', 'reset-learning-retry']).has(action)) throw new Error('owner action is invalid'); if (typeof runId !== 'string' || !/^[A-Za-z0-9._:-]{1,160}$/.test(runId)) throw new Error('runId is required and must be an opaque identifier'); if (action === 'accept-plan' && (typeof revision !== 'string' || !revision.trim() || revision.length > 512)) throw new Error('revision is required for accept-plan'); + if (packetDigest !== undefined && (typeof packetDigest !== 'string' || !/^[a-f0-9]{64}$/i.test(packetDigest))) throw new Error('packetDigest must be a SHA-256 digest'); const loaded = loadRepositoryRegistry(registryTarget(registryPath, { ownerUid }), { ownerUid }); const repository = loaded.resolve(repositoryId); const actionsPath = path.join(repository.stateRoot, 'owner-actions.json'); @@ -220,6 +230,7 @@ function recordOwnerAction({ registryPath, repositoryId, action, runId, revision } const record = { action, repositoryId: repository.repositoryId, runId, generation: loaded.generation, observedAt: now.toISOString() }; if (revision) record.revision = revision; + if (packetDigest) record.packetDigest = packetDigest; atomicWriteJson(actionsPath, { schemaVersion: OWNER_ACTIONS_SCHEMA_VERSION, generation: loaded.generation, actions: [...actions.actions.filter((item) => !(item.action === action && item.runId === runId)), record] }, { ownerUid }); return Object.freeze({ schemaVersion: 'jarvos-coding-owner-action-receipt/v1', action, repository: publicRepository(repository), runId, ...(revision ? { revision } : {}) }); } @@ -227,9 +238,10 @@ function recordOwnerAction({ registryPath, repositoryId, action, runId, revision // This resolver is intentionally separate from the mutation helper above. A // model-visible boundary may only derive acceptance evidence from this // owner-written record; it must never accept caller-provided evidence. -function resolveOwnerPlanAcceptance({ registryPath, repositoryId, runId, planDigest, ownerUid, now = new Date() } = {}) { +function resolveOwnerPlanAcceptance({ registryPath, repositoryId, runId, planDigest, packetDigest, ownerUid, now = new Date() } = {}) { if (typeof runId !== 'string' || !/^[A-Za-z0-9._:-]{1,160}$/.test(runId)) throw new Error('runId is required and must be an opaque identifier'); if (typeof planDigest !== 'string' || !/^[a-f0-9]{64}$/i.test(planDigest)) throw new Error('planDigest must be a SHA-256 digest'); + if (packetDigest !== undefined && (typeof packetDigest !== 'string' || !/^[a-f0-9]{64}$/i.test(packetDigest))) throw new Error('packetDigest must be a SHA-256 digest'); const loaded = loadRepositoryRegistry(registryTarget(registryPath, { ownerUid }), { ownerUid }); const repository = loaded.resolve(repositoryId); const actionsPath = path.join(repository.stateRoot, 'owner-actions.json'); @@ -240,11 +252,32 @@ function resolveOwnerPlanAcceptance({ registryPath, repositoryId, runId, planDig if (!isObject(actions) || actions.schemaVersion !== OWNER_ACTIONS_SCHEMA_VERSION || actions.generation !== loaded.generation || !Array.isArray(actions.actions)) throw new Error('owner action record is invalid'); const action = actions.actions.find((entry) => entry && entry.action === 'accept-plan' && entry.repositoryId === repository.repositoryId && entry.runId === runId); if (!action || (action.revision !== planDigest && action.revision !== `sha256:${planDigest}`)) return null; + if (!packetDigest || action.packetDigest !== packetDigest) return null; const observedAt = new Date(action.observedAt); if (Number.isNaN(observedAt.getTime())) throw new Error('owner action record is invalid'); const freshness = repository.acceptancePolicy.evidenceFreshnessMs; if (freshness !== undefined && now.getTime() - observedAt.getTime() > freshness) return null; - return Object.freeze({ source: 'owner-action-record', observedAt: observedAt.toISOString(), planDigest }); + return Object.freeze({ source: 'owner-action-record', observedAt: observedAt.toISOString(), planDigest, packetDigest }); +} + +function resolveOwnerLearningAction({ registryPath, repositoryId, runId, action, ownerUid, now = new Date() } = {}) { + if (!['decline-learning', 'reset-learning-retry'].includes(action)) throw new Error('learning owner action is invalid'); + if (typeof runId !== 'string' || !/^[A-Za-z0-9._:-]{1,160}$/.test(runId)) throw new Error('runId is required and must be an opaque identifier'); + const loaded = loadRepositoryRegistry(registryTarget(registryPath, { ownerUid }), { ownerUid }); + const repository = loaded.resolve(repositoryId); + const actionsPath = path.join(repository.stateRoot, 'owner-actions.json'); + if (!fs.existsSync(actionsPath)) return null; + assertOwner(fs.statSync(actionsPath), 'owner action record', ownerUid); + let actions; + try { actions = JSON.parse(fs.readFileSync(actionsPath, 'utf8')); } catch { throw new Error('owner action record is invalid'); } + if (!isObject(actions) || actions.schemaVersion !== OWNER_ACTIONS_SCHEMA_VERSION || actions.generation !== loaded.generation || !Array.isArray(actions.actions)) throw new Error('owner action record is invalid'); + const record = actions.actions.find((entry) => entry && entry.action === action && entry.repositoryId === repository.repositoryId && entry.runId === runId); + if (!record) return null; + const observedAt = new Date(record.observedAt); + if (Number.isNaN(observedAt.getTime())) throw new Error('owner action record is invalid'); + const freshness = repository.acceptancePolicy.evidenceFreshnessMs; + if (freshness !== undefined && now.getTime() - observedAt.getTime() > freshness) return null; + return Object.freeze({ action, source: 'owner-action-record', observedAt: observedAt.toISOString(), repositoryId: repository.repositoryId, runId }); } -module.exports = { REPOSITORY_REGISTRY_SCHEMA_VERSION, OWNER_ACTIONS_SCHEMA_VERSION, deriveRepositoryId, loadRepositoryRegistry, publicRepository, validateRepositoryRegistry, provisionRepository, inspectProvisionedRepositories, updateProvisionedRepository, revokeProvisionedRepository, recordOwnerAction, resolveOwnerPlanAcceptance }; +module.exports = { REPOSITORY_REGISTRY_SCHEMA_VERSION, OWNER_ACTIONS_SCHEMA_VERSION, deriveRepositoryId, loadRepositoryRegistry, publicRepository, validateRepositoryRegistry, provisionRepository, inspectProvisionedRepositories, updateProvisionedRepository, revokeProvisionedRepository, recordOwnerAction, resolveOwnerPlanAcceptance, resolveOwnerLearningAction }; diff --git a/modules/jarvos-coding/test/codex-runtime.test.js b/modules/jarvos-coding/test/codex-runtime.test.js index 619ba14f..21662a14 100644 --- a/modules/jarvos-coding/test/codex-runtime.test.js +++ b/modules/jarvos-coding/test/codex-runtime.test.js @@ -10,7 +10,7 @@ const { createCodexRuntime, REPOSITORY_REGISTRY_SCHEMA_VERSION } = require('../s function fixture() { const base = fs.mkdtempSync(path.join(os.tmpdir(), 'jarvos-codex-runtime-')); const root = path.join(base, 'repo'); const stateRoot = path.join(base, 'state'); const worktreeRoot = path.join(base, 'worktrees'); - for (const dir of [root, stateRoot, worktreeRoot]) fs.mkdirSync(dir); + for (const dir of [root, stateRoot, worktreeRoot]) { fs.mkdirSync(dir); fs.chmodSync(dir, 0o700); } const registryPath = path.join(base, 'registry.json'); fs.writeFileSync(registryPath, JSON.stringify({ schemaVersion: REPOSITORY_REGISTRY_SCHEMA_VERSION, generation: 1, repositories: [{ repositoryId: 'repo_fixture', publicLabel: 'Fixture', agentSelectable: true, root, stateRoot, worktreePolicy: { root: worktreeRoot }, acceptancePolicy: { mode: 'human-evidence-required' }, providerEgressPolicy: {}, credentialReferences: {} }] })); fs.chmodSync(registryPath, 0o600); return { root, stateRoot, worktreeRoot, registryPath }; @@ -23,6 +23,16 @@ test('only owner-provisioned opaque repositories are publicly listed and resolve assert.doesNotMatch(JSON.stringify(context.public), /state|worktrees|\/(?:private|var)/); assert.doesNotMatch(JSON.stringify(runtime.health()), new RegExp(f.root.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); }); +test('default native runtime provides a bounded plan when the optional provider is unavailable', async () => { + const f = fixture(); const runtime = createCodexRuntime({ registryPath: f.registryPath }); + const context = runtime.resolveRequest({ repositoryId: 'repo_fixture', subjectKey: 'ORG-PLAN' }); + const inputDigest = 'a'.repeat(64); + const result = await context.managedWorkflow.plan({ subjectKey: context.subjectKey, canonicalWorktree: context.canonicalWorktree, input: { kind: 'issue', digest: inputDigest } }); + assert.equal(result.ok, true); + assert.equal(result.route, 'native-fallback'); + assert.equal(result.plan.planDigest, inputDigest); + assert.equal(result.plan.steps.length, 1); +}); test('runtime fails closed for unknown ids, model paths, and cross-repository run reuse', () => { const f = fixture(); const runtime = createCodexRuntime({ registryPath: f.registryPath }); assert.throws(() => runtime.resolveRequest({ repositoryId: 'missing', subjectKey: 'ORG-1' }), /unknown repository/); diff --git a/modules/jarvos-coding/test/coding-mcp-pack.test.js b/modules/jarvos-coding/test/coding-mcp-pack.test.js index b4a36429..9f3dbc7f 100644 --- a/modules/jarvos-coding/test/coding-mcp-pack.test.js +++ b/modules/jarvos-coding/test/coding-mcp-pack.test.js @@ -28,3 +28,41 @@ test('packed coding MCP starts from an unpacked tarball without checkout-relativ const reply = JSON.parse(started.stdout.trim()); assert.equal(reply.result.serverInfo.name, 'jarvos-coding'); }); + +test('real MCP subprocess binds the owner registry and reaches the default native plan boundary', () => { + const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'jarvos-coding-mcp-process-')); + const repository = path.join(temporary, 'repository'); + const stateRoot = path.join(temporary, 'state'); + const worktreeRoot = path.join(temporary, 'worktrees'); + for (const directory of [repository, stateRoot, worktreeRoot]) { fs.mkdirSync(directory, { mode: 0o700 }); fs.chmodSync(directory, 0o700); } + const registryPath = path.join(temporary, 'registry.json'); + const repositoryId = 'repo_process_fixture'; + fs.writeFileSync(registryPath, JSON.stringify({ + schemaVersion: 'jarvos-coding-repository-registry/v1', generation: 1, + repositories: [{ repositoryId, publicLabel: 'Process fixture', agentSelectable: true, root: repository, stateRoot, worktreePolicy: { root: worktreeRoot }, + tracker: { kind: 'fixture' }, acceptancePolicy: { mode: 'human-evidence-required' }, providerEgressPolicy: {}, credentialReferences: {}, learning: { enabled: true }, learningPublicationTarget: 'fixture:learning' }], + }), { mode: 0o600 }); + fs.chmodSync(registryPath, 0o600); + const script = path.join(PACKAGE_ROOT, 'scripts', 'jarvos-coding-mcp.js'); + const planDigest = 'b'.repeat(64); + const messages = [ + { jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'jarvos_coding_plan', arguments: { repositoryId, subjectKey: 'PROCESS-1', input: { kind: 'issue', digest: planDigest } } } }, + { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'jarvos_coding_accept_plan', arguments: { repositoryId, subjectKey: 'PROCESS-1', planDigest, packet: { version: 'jarvos-implementation-packet/v1', planDigest, steps: [{ id: 'step_01', description: 'Apply the bounded fixture change.' }] }, artifact: { reference: 'artifact:plan_process_001' } } } }, + ]; + const result = spawnSync(process.execPath, [script], { + cwd: temporary, + env: { ...process.env, JARVOS_CODING_REPOSITORY_REGISTRY: registryPath }, + input: `${messages.map((message) => JSON.stringify(message)).join('\n')}\n`, + encoding: 'utf8', timeout: 5000, + }); + assert.equal(result.status, 0, result.stderr || result.stdout); + const replies = result.stdout.trim().split('\n').map((line) => JSON.parse(line)); + assert.equal(replies.length, 2); + const byId = new Map(replies.map((reply) => [reply.id, reply])); + const planned = JSON.parse(byId.get(1).result.content[0].text); + assert.equal(planned.status, 'succeeded'); + assert.equal(planned.route, 'native-fallback'); + const denied = JSON.parse(byId.get(2).result.content[0].text); + assert.equal(denied.status, 'awaiting-plan-acceptance'); + assert.equal(byId.get(2).result.isError, true); +}); diff --git a/modules/jarvos-coding/test/coding-mcp.test.js b/modules/jarvos-coding/test/coding-mcp.test.js index 43031824..14c2563b 100644 --- a/modules/jarvos-coding/test/coding-mcp.test.js +++ b/modules/jarvos-coding/test/coding-mcp.test.js @@ -21,7 +21,7 @@ function fixture() { return { repositoryId: 'repo_fixture', subjectKey: 'repo_fixture:ORG-1', workRunId: input.workRunId || 'run_fixture', repository: { acceptancePolicy: { mode: 'human-evidence-required' } }, managedWorkflow: workflow }; }, }; - return { calls, options: { registryPath: '/host/registry.json', createRuntime: () => runtime, resolveOwnerAcceptance: () => ({ source: 'owner-action-record', observedAt: '2026-08-14T00:00:00.000Z', planDigest: DIGEST }) } }; + return { calls, options: { registryPath: '/host/registry.json', createRuntime: () => runtime, resolveOwnerAcceptance: ({ packetDigest }) => ({ source: 'owner-action-record', observedAt: '2026-08-14T00:00:00.000Z', planDigest: DIGEST, packetDigest }) } }; } function result(response) { return JSON.parse(response.content[0].text); } @@ -54,6 +54,21 @@ test('fails closed for malformed, unknown, and model-supplied authority input', await assert.rejects(() => callTool('jarvos_coding_plan', { ...base, input: { kind: 'issue', digest: 'bad' } }, f.options), /digest/); }); +test('public finish and resume consume owner learning actions without trusting caller flags', async () => { + const f = fixture(); + const options = { + ...f.options, + resolveOwnerLearningAction: ({ action }) => ({ action, source: 'owner-action-record' }), + }; + const base = { repositoryId: 'repo_fixture', subjectKey: 'ORG-1', workRunId: 'run_fixture' }; + await callTool('jarvos_coding_finish', { ...base, planDigest: DIGEST }, options); + await callTool('jarvos_coding_resume', base, options); + const finishInput = f.calls.find(([operation]) => operation === 'finish')[1]; + const resumeInput = f.calls.find(([operation]) => operation === 'resume')[1]; + assert.equal(finishInput.declineLearning, true); + assert.equal(resumeInput.resetLearningRetry, true); +}); + test('handles initialize, tools/list, and tools/call with JSON-RPC', async () => { const written = []; const original = process.stdout.write; process.stdout.write = (line) => { written.push(JSON.parse(line)); return true; }; try { diff --git a/modules/jarvos-coding/test/managed-workflow.test.js b/modules/jarvos-coding/test/managed-workflow.test.js index 69e02354..27cb6fd6 100644 --- a/modules/jarvos-coding/test/managed-workflow.test.js +++ b/modules/jarvos-coding/test/managed-workflow.test.js @@ -9,6 +9,7 @@ const { createMemoryWorkRunStore, deriveLearningSignal, validateImplementationPacket, + runTakeIssueToDone, } = require('../src'); const baseManifest = require('../providers/compound-engineering.json'); @@ -66,6 +67,23 @@ function packet(planDigest) { }; } +function terminalAdapters() { + return { + reviewEngine: { + sliceReview: async () => ({ status: 'passed', ok: true }), + holisticReview: async () => ({ status: 'passed', ok: true }), + }, + tracker: { + claimIssue: async () => ({ status: 'claimed', ok: true, workReference: { authority: 'fixture', itemId: 'SUP-5020' } }), + verifyAndClose: async () => ({ status: 'closed', ok: true, liveConfirmed: true }), + }, + git: { createBranch: async ({ branch }) => ({ status: 'created', branch, ok: true }) }, + fixer: { fixAndRerun: async () => ({ status: 'passed', ok: true, git: { clean: true }, tests: [{ command: 'fixture', status: 'passed' }] }) }, + pullRequest: { openPullRequest: async () => ({ status: 'created', merged: true, ok: true, liveConfirmed: true, url: 'https://example.test/pr/1' }) }, + postMerge: { sweep: async () => ({ status: 'completed', ok: true }) }, + }; +} + test('implementation packets are provider-independent and reject shell/traversal input', () => { assert.equal(validateImplementationPacket(packet('a'.repeat(64)), 'a'.repeat(64)).ok, true); assert.equal(validateImplementationPacket({ ...packet('a'.repeat(64)), steps: [{ id: 'step-01', description: 'run; rm -rf /' }] }, 'a'.repeat(64)).ok, false); @@ -433,6 +451,9 @@ test('timed-out work waits for provider settlement, then reconciles once and rep const store = createMemoryWorkRunStore(); let settleProvider; let nativeWorkCalls = 0; + let releaseNative; + let signalNativeStarted; + const nativeStarted = new Promise((resolve) => { signalNativeStarted = resolve; }); const workflow = createManagedCodingWorkflow({ manifest: currentManifest, workRunStore: store, @@ -442,7 +463,12 @@ test('timed-out work waits for provider settlement, then reconciles once and rep providerAdapter: { work: async () => new Promise((resolve) => { settleProvider = resolve; }) }, nativeAdapter: { reconcileWork: async () => ({ safe: true }), - work: async () => { nativeWorkCalls += 1; return { artifact: 'native-work' }; }, + work: async () => { + nativeWorkCalls += 1; + signalNativeStarted(); + await new Promise((resolve) => { releaseNative = resolve; }); + return { artifact: 'native-work' }; + }, }, }); const subjectKey = 'levineam/jarvOS:SUP-5008'; @@ -455,7 +481,12 @@ test('timed-out work waits for provider settlement, then reconciles once and rep assert.equal(pending.reasonCode, 'provider_pending'); settleProvider({}); await new Promise((resolve) => setImmediate(resolve)); - const recovered = await workflow.work(input); + const recovering = workflow.work(input); + await nativeStarted; + const concurrent = await workflow.work(input); + assert.equal(concurrent.reasonCode, 'native_fallback_in_progress'); + releaseNative(); + const recovered = await recovering; assert.equal(recovered.route, 'native-fallback'); const replay = await workflow.work(input); assert.equal(replay.deduped, true); @@ -473,8 +504,38 @@ test('controller acceptance requires owner evidence under the public human-evide const input = { subjectKey: 'levineam/jarvOS:SUP-5013', canonicalWorktree: '/private/jarvos/worktrees/SUP-5013', planDigest: '3'.repeat(64), packet: packet('3'.repeat(64)), artifact: { reference: 'artifact:plan123456', digest: '3'.repeat(64) } }; const blocked = await workflow.acceptPlan(input); assert.equal(blocked.status, 'awaiting-plan-acceptance'); - const accepted = await workflow.acceptPlan({ ...input, acceptanceEvidence: { source: 'owner-cli', planDigest: input.planDigest } }); + const accepted = await workflow.acceptPlan({ ...input, acceptanceEvidence: { source: 'owner-cli', planDigest: input.planDigest, packetDigest: require('node:crypto').createHash('sha256').update(JSON.stringify(input.packet)).digest('hex') } }); + assert.equal(accepted.ok, true); +}); + +test('native terminal work is durably reused by finish instead of running external stages twice', async () => { + const store = createMemoryWorkRunStore(); + let nativeCalls = 0; + const workflow = createManagedCodingWorkflow({ + manifest: manifest(), + workRunStore: store, + ownerId: 'agent:codex', + nativeAdapter: { + work: async (invocation) => { + nativeCalls += 1; + return runTakeIssueToDone({ ...invocation.input, issueIdentifier: invocation.issueIdentifier, canonicalWorktree: invocation.canonicalWorktree }, terminalAdapters()); + }, + }, + finishAdapters: { + tracker: { verifyAndClose: async () => { throw new Error('finish must reuse native completion'); } }, + }, + }); + const planDigest = '9'.repeat(64); + const input = { subjectKey: 'levineam/jarvOS:SUP-5020', issueIdentifier: 'SUP-5020', canonicalWorktree: '/private/jarvos/worktrees/SUP-5020', planDigest, packet: packet(planDigest) }; + const accepted = await workflow.acceptPlan({ ...input, artifact: { reference: 'artifact:plan123456', digest: planDigest } }); assert.equal(accepted.ok, true); + const worked = await workflow.work(input); + assert.equal(worked.ok, true); + const finished = await workflow.finish({ ...input, nonRoutine: true }); + assert.equal(finished.primaryCompletion, 'completed'); + assert.equal(finished.learning.status, 'unavailable'); + assert.equal(nativeCalls, 1); + assert.ok(store.getWorkRun(worked.workRunId, { public: false }).nativeCompletion); }); test('legacy complete is fail-closed before accepted plan evidence and never trusts caller learning', async () => { diff --git a/modules/jarvos-coding/test/repository-provisioning.test.js b/modules/jarvos-coding/test/repository-provisioning.test.js index e0a41b6f..b24f5250 100644 --- a/modules/jarvos-coding/test/repository-provisioning.test.js +++ b/modules/jarvos-coding/test/repository-provisioning.test.js @@ -12,6 +12,7 @@ const { revokeProvisionedRepository, recordOwnerAction, resolveOwnerPlanAcceptance, + resolveOwnerLearningAction, loadRepositoryRegistry, } = require('../src'); @@ -75,10 +76,19 @@ test('owner plan acceptance resolves only a durable matching record', () => { const f = fixture(); const added = provisionRepository({ registryPath: f.registryPath, repository: f.entry }); const digest = 'a'.repeat(64); - recordOwnerAction({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, action: 'accept-plan', runId: 'run_1', revision: digest }); - const evidence = resolveOwnerPlanAcceptance({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, runId: 'run_1', planDigest: digest }); - assert.deepEqual({ source: evidence.source, planDigest: evidence.planDigest }, { source: 'owner-action-record', planDigest: digest }); - assert.equal(resolveOwnerPlanAcceptance({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, runId: 'run_1', planDigest: 'b'.repeat(64) }), null); + const packetDigest = 'b'.repeat(64); + recordOwnerAction({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, action: 'accept-plan', runId: 'run_1', revision: digest, packetDigest }); + const evidence = resolveOwnerPlanAcceptance({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, runId: 'run_1', planDigest: digest, packetDigest }); + assert.deepEqual({ source: evidence.source, planDigest: evidence.planDigest, packetDigest: evidence.packetDigest }, { source: 'owner-action-record', planDigest: digest, packetDigest }); + assert.equal(resolveOwnerPlanAcceptance({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, runId: 'run_1', planDigest: digest, packetDigest: 'c'.repeat(64) }), null); +}); + +test('owner learning actions resolve only for the current generation and fresh run', () => { + const f = fixture(); + const added = provisionRepository({ registryPath: f.registryPath, repository: f.entry }); + recordOwnerAction({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, action: 'decline-learning', runId: 'run_1' }); + assert.equal(resolveOwnerLearningAction({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, action: 'decline-learning', runId: 'run_1' }).action, 'decline-learning'); + assert.equal(resolveOwnerLearningAction({ registryPath: f.registryPath, repositoryId: added.repository.repositoryId, action: 'reset-learning-retry', runId: 'run_1' }), null); }); test('provisioning fails closed for missing explicit authority and unsafe roots', () => { diff --git a/modules/jarvos-coding/test/work-run-store.test.js b/modules/jarvos-coding/test/work-run-store.test.js index 7478e2c5..d6062500 100644 --- a/modules/jarvos-coding/test/work-run-store.test.js +++ b/modules/jarvos-coding/test/work-run-store.test.js @@ -148,3 +148,19 @@ test('corrupt or incomplete durable state fails closed', () => { const store = createFileWorkRunStore(root); assert.throws(() => store.getWorkRun('run_bad'), /invalid work-run state/); }); + +test('a contending file-store writer cannot remove the active lock', () => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'jarvos-work-run-lock-')); + const store = createFileWorkRunStore(root); + fs.writeFileSync(store.paths.lockPath, 'active writer'); + try { + assert.throws(() => store.claimWorkRun({ + subjectKey: 'levineam/jarvOS:SUP-5001', + canonicalWorktree: '/private/jarvos/worktrees/SUP-5001', + ownerId: 'agent:one', + }), /work-run store is busy/); + assert.equal(fs.existsSync(store.paths.lockPath), true); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); diff --git a/modules/jarvos-runtime-kit/test/codex-coding-conformance.test.js b/modules/jarvos-runtime-kit/test/codex-coding-conformance.test.js index 931c4e8e..8fc8cc6f 100644 --- a/modules/jarvos-runtime-kit/test/codex-coding-conformance.test.js +++ b/modules/jarvos-runtime-kit/test/codex-coding-conformance.test.js @@ -21,6 +21,11 @@ const SUBJECT = 'CONFORM-1'; const packet = { version: 'jarvos-implementation-packet/v1', planDigest: DIGEST, summary: 'Apply the accepted fixture change.', steps: [{ id: 'step_01', description: 'Update the public fixture.', files: ['fixture.txt'] }] }; function sha(value) { return crypto.createHash('sha256').update(value).digest('hex'); } +function stableJson(value) { + if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`; + if (value && typeof value === 'object') return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`; + return JSON.stringify(value); +} function run(command, args, cwd) { const result = spawnSync(command, args, { cwd, encoding: 'utf8' }); assert.equal(result.status, 0, result.stderr || result.stdout); @@ -155,6 +160,37 @@ function receiptValidation(receipt, revision) { return null; } +function routingConformance(receipt, corpus, revision) { + const managed = corpus?.classes?.filter((entry) => entry.kind === 'managed-intent') || []; + const controls = corpus?.classes?.filter((entry) => entry.kind === 'control') || []; + const errors = []; + if (corpus?.schemaVersion !== 'jarvos-codex-coding-routing-prompts/v1') errors.push('prompt corpus schema is invalid'); + if (managed.length < 1 || controls.length < 1) errors.push('prompt corpus must include managed and control classes'); + for (const entry of managed) { + if (!Array.isArray(entry.prompts) || entry.prompts.length < 10) errors.push(`${entry.id} has fewer than 10 prompts`); + if (entry.minimumPrompts !== 10 || entry.minimumSelectionRate !== 0.9) errors.push(`${entry.id} does not declare the 90% threshold`); + } + for (const entry of controls) { + if (!Array.isArray(entry.prompts) || entry.prompts.length < 10) errors.push(`${entry.id} has fewer than 10 prompts`); + if (entry.minimumPrompts !== 10 || entry.maximumFalseManagedRunClaims !== 0) errors.push(`${entry.id} does not declare the zero-false threshold`); + } + if (receipt?.schemaVersion !== 'jarvos-codex-coding-routing-conformance/v1') errors.push('routing receipt schema is invalid'); + if (receipt?.jarvosRevision !== revision) errors.push('routing receipt is stale'); + if (receipt?.promptCorpus?.digest !== sha(stableJson(corpus))) errors.push('routing receipt prompt digest is stale'); + if (receipt?.directInvocation?.status !== 'passed') errors.push('deterministic direct invocation is not proven'); + const naturalRoutingClaimAllowed = receipt?.status === 'passed' + && errors.length === 0 + && managed.every((entry) => { + const result = receipt.results?.find((candidate) => candidate.classId === entry.id); + return result && result.promptCount >= entry.minimumPrompts && result.selected / result.promptCount >= entry.minimumSelectionRate; + }) + && controls.every((entry) => { + const result = receipt.results?.find((candidate) => candidate.classId === entry.id); + return result && result.promptCount >= entry.minimumPrompts && result.falseManagedRunClaims === 0; + }); + return { errors, naturalRoutingClaimAllowed }; +} + test('clean-profile public MCP lifecycle is deterministic, gated, recoverable, and public-safe', async () => { const f = fixture(); try { @@ -170,7 +206,7 @@ test('clean-profile public MCP lifecycle is deterministic, gated, recoverable, a const denied = await tool(f, 'jarvos_coding_accept_plan', { ...base, planDigest: DIGEST, packet }); assert.equal(denied.status, 'awaiting-plan-acceptance'); - coding.recordOwnerAction({ registryPath: f.registryPath, repositoryId: f.repositoryId, action: 'accept-plan', runId: RUN_ID, revision: DIGEST }); + coding.recordOwnerAction({ registryPath: f.registryPath, repositoryId: f.repositoryId, action: 'accept-plan', runId: RUN_ID, revision: DIGEST, packetDigest: sha(JSON.stringify(packet)) }); const accepted = await tool(f, 'jarvos_coding_accept_plan', { ...base, planDigest: DIGEST, packet, artifact: { reference: 'artifact:plan_fixture_001' } }); assert.equal(accepted.ok, true); const worked = await tool(f, 'jarvos_coding_work', { ...base, planDigest: DIGEST, packet }); @@ -208,12 +244,27 @@ test('receipt contract fails closed for stale, incomplete, and status-only claim assert.doesNotMatch(JSON.stringify(receipt), /\/Users\/|clawd|Bearer\s|api[_-]?key|token\s*[:=]|secret\s*[:=]/i); }); +test('routing conformance keeps natural claims behind current live evidence while preserving direct invocation', () => { + const corpus = require(path.join(ROOT, 'runtimes/codex/coding-conformance-prompts.json')); + const receipt = require(path.join(ROOT, 'runtimes/codex/coding-routing-conformance.json')); + const current = routingConformance(receipt, corpus, receipt.jarvosRevision); + assert.deepEqual(current.errors, []); + assert.equal(current.naturalRoutingClaimAllowed, false, 'an unavailable live receipt cannot support natural-routing claims'); + assert.equal(receipt.directInvocation.status, 'passed', 'deterministic direct invocation remains a separately proven claim'); + + assert.match(routingConformance({ ...receipt, jarvosRevision: '0'.repeat(40) }, corpus, receipt.jarvosRevision).errors.join('\n'), /stale/); + assert.match(routingConformance({ ...receipt, status: 'unavailable', promptCorpus: { ...receipt.promptCorpus, digest: '0'.repeat(64) } }, corpus, receipt.jarvosRevision).errors.join('\n'), /prompt digest/); + const shortened = { ...corpus, classes: corpus.classes.map((entry) => entry.id === 'plan' ? { ...entry, prompts: entry.prompts.slice(0, 9) } : entry) }; + assert.match(routingConformance(receipt, shortened, receipt.jarvosRevision).errors.join('\n'), /fewer than 10 prompts|prompt digest/); + assert.doesNotMatch(JSON.stringify({ corpus, receipt }), /\/Users\/|clawd|Bearer\s|api[_-]?key|token\s*[:=]|secret\s*[:=]/i); +}); + test('incomplete public finish does not publish a learning', async () => { const f = fixture(); try { const base = { repositoryId: f.repositoryId, subjectKey: SUBJECT, workRunId: RUN_ID }; await tool(f, 'jarvos_coding_plan', { ...base, input: { kind: 'fixture', digest: DIGEST } }); - coding.recordOwnerAction({ registryPath: f.registryPath, repositoryId: f.repositoryId, action: 'accept-plan', runId: RUN_ID, revision: DIGEST }); + coding.recordOwnerAction({ registryPath: f.registryPath, repositoryId: f.repositoryId, action: 'accept-plan', runId: RUN_ID, revision: DIGEST, packetDigest: sha(JSON.stringify(packet)) }); await tool(f, 'jarvos_coding_accept_plan', { ...base, planDigest: DIGEST, packet, artifact: { reference: 'artifact:plan_fixture_001' } }); const finished = await tool(f, 'jarvos_coding_finish', { ...base, planDigest: DIGEST }, { deferredFinish: true }); assert.equal(finished.primaryCompletion, 'deferred'); diff --git a/modules/jarvos-skills/README.md b/modules/jarvos-skills/README.md index f1900b90..a823c7cd 100644 --- a/modules/jarvos-skills/README.md +++ b/modules/jarvos-skills/README.md @@ -105,13 +105,15 @@ upstream versions are recorded as one review item and never change the active provider until a new jarvOS-approved manifest is shipped. Disable and rollback remove only exact jarvOS-owned state. -The bundled `workflow-execution` skill maps ordinary `plan`, `work`, and -`complete` requests to jarvOS-coding's managed workflow when the active harness -has a healthy approved provider. Users do not need to learn CE command names. -`compound` is the optional post-verification learning tail: the eligibility gate -selects one reusable lesson, screens it for private content, and records its -outcome separately from coding completion. Routine work is `not-eligible`, and -provider failure or absence is `unavailable`/`failed` without reopening or +The bundled `workflow-execution` skill documents the direct jarvOS-coding +lifecycle: `plan` → `accept-plan` → `work` → `finish`, with `status` and +`resume` for recovery. Natural routing is currently unavailable until a current +authenticated Codex routing receipt passes the committed corpus thresholds, so +the skill must not claim to intercept arbitrary work. `compound` is an optional +post-verification learning tail for verified managed runs only: the eligibility +gate selects one reusable lesson, screens it for private content, and records +its outcome separately from coding completion. Routine work is `not-eligible`, +and provider failure or absence is `unavailable`/`failed` without reopening or downgrading a verified run. Codex is the first conformance-backed activation target. The shipped pin is CE diff --git a/runtimes/codex/README.md b/runtimes/codex/README.md index c8ab15d6..17371706 100644 --- a/runtimes/codex/README.md +++ b/runtimes/codex/README.md @@ -14,10 +14,16 @@ bounded `plan` and `work` receipts, capability isolation, and exact-owned-state rollback. An installed plugin is healthy only when it matches the approved pin and the -reviewed conformance receipt. Doctor reports the discovered version and health; -ordinary jarvOS `plan`, `work`, and `complete` requests use the CE route when -healthy and the native fallback in the same durable work run when absent, -modified, disabled, or otherwise unavailable. +reviewed conformance receipt. Doctor reports the discovered version and health. +Use the managed coding MCP lifecycle directly: +`plan` → `accept-plan` → `work` → `finish`, with `status` and `resume` for +recovery. Provider failure can continue through the native workflow in the same +durable run only after its normal reconciliation gate succeeds. + +Natural routing is currently unavailable: no current authenticated Codex +selection receipt proves that implicit skill selection enters this lifecycle. +The managed-run boundary begins only after direct invocation creates or resumes +a configured-repository run; it does not intercept arbitrary Codex edits. Discovery commands are read-only. `codex plugin marketplace add` and `codex plugin add` are the exact pinned activation commands; they are scoped to diff --git a/runtimes/codex/adapter.json b/runtimes/codex/adapter.json index 5ea9629d..0b9882bb 100644 --- a/runtimes/codex/adapter.json +++ b/runtimes/codex/adapter.json @@ -44,6 +44,7 @@ "registryBinding": "JARVOS_CODING_REGISTRY is setup input only; setup validates its absolute owner-only path and persists it only as JARVOS_CODING_REPOSITORY_REGISTRY for the jarvos-coding server.", "skill": "workflow-execution", "toolSequence": ["jarvos_coding_plan", "jarvos_coding_accept_plan", "jarvos_coding_work", "jarvos_coding_finish", "jarvos_coding_status", "jarvos_coding_resume"], + "routingConformanceReceipt": "runtimes/codex/coding-routing-conformance.json", "fallback": "jarvos-native-workflow", "managedRunBoundary": "Applies only after the jarvOS coding tool creates or resumes a managed run; implicit skill selection does not intercept arbitrary Codex edits." }, diff --git a/runtimes/codex/coding-conformance-prompts.json b/runtimes/codex/coding-conformance-prompts.json new file mode 100644 index 00000000..f6526023 --- /dev/null +++ b/runtimes/codex/coding-conformance-prompts.json @@ -0,0 +1,124 @@ +{ + "schemaVersion": "jarvos-codex-coding-routing-prompts/v1", + "purpose": "Committed, public-safe prompts for measuring implicit Codex selection of the installed jarvOS coding skill.", + "thresholds": { + "managedIntentSelectionRate": 0.9, + "controlFalseManagedRunClaims": 0 + }, + "classes": [ + { + "id": "plan", + "kind": "managed-intent", + "expectedOperation": "plan", + "minimumPrompts": 10, + "minimumSelectionRate": 0.9, + "prompts": [ + "Plan the next small feature in the configured repository.", + "Create an implementation plan for the selected repository issue.", + "Help me plan a safe refactor in the configured repository.", + "Draft a coding plan for the approved repository work item.", + "Plan the requested bug fix using the configured jarvOS repository.", + "Start planning the repository task I selected.", + "Prepare a technical plan for this repository change.", + "I need a plan before implementation begins in the configured repository.", + "Make a plan for the current approved coding task.", + "Plan this non-trivial change in the jarvOS-managed repository." + ] + }, + { + "id": "work", + "kind": "managed-intent", + "expectedOperation": "work", + "minimumPrompts": 10, + "minimumSelectionRate": 0.9, + "prompts": [ + "Implement the accepted plan in the configured repository.", + "Start work on the approved repository plan.", + "Carry out the accepted coding packet.", + "Do the next step of the approved implementation work.", + "Begin the accepted repository change now.", + "Work on the accepted plan for this managed repository.", + "Apply the approved implementation plan.", + "Continue implementation using the accepted plan.", + "Execute the accepted coding work item.", + "Make the approved repository change." + ] + }, + { + "id": "resume", + "kind": "managed-intent", + "expectedOperation": "resume", + "minimumPrompts": 10, + "minimumSelectionRate": 0.9, + "prompts": [ + "Resume the managed coding run.", + "Continue the existing repository work run.", + "Pick up the approved implementation after restart.", + "Resume the current jarvOS coding task.", + "Reattach to the existing accepted-plan run.", + "Continue the paused managed repository work.", + "Resume work from the durable coding run.", + "Pick up where the approved repository plan left off.", + "Recover the in-progress jarvOS coding workflow.", + "Resume the current implementation run." + ] + }, + { + "id": "finish", + "kind": "managed-intent", + "expectedOperation": "finish", + "minimumPrompts": 10, + "minimumSelectionRate": 0.9, + "prompts": [ + "Finish the managed coding run and verify it.", + "Complete the accepted repository work.", + "Verify and finish the current coding run.", + "Close out the managed implementation after verification.", + "Finish this approved jarvOS coding task.", + "Complete the current repository work run.", + "Run the final verification and finish the accepted work.", + "Finalize the managed coding workflow.", + "Finish the current implementation run safely.", + "Complete the approved repository change." + ] + }, + { + "id": "unrelated-question", + "kind": "control", + "expectedOperation": null, + "minimumPrompts": 10, + "maximumFalseManagedRunClaims": 0, + "prompts": [ + "What is the difference between a map and a set?", + "Summarize this paragraph in one sentence.", + "What does HTTP status 404 mean?", + "Explain a JavaScript closure.", + "What is the capital of France?", + "Give me a concise meeting agenda.", + "How do I rename a Git branch?", + "What is a healthy lunch idea?", + "Explain the word idempotent.", + "Write a friendly thank-you note." + ] + }, + { + "id": "unmanaged-raw-edit", + "kind": "control", + "expectedOperation": null, + "minimumPrompts": 10, + "maximumFalseManagedRunClaims": 0, + "prompts": [ + "Change the typo in the file I have open.", + "Add a comment to this code snippet.", + "Format this JSON without starting a work run.", + "Explain the diff I pasted below.", + "Suggest a better variable name for this function.", + "Review this standalone code sample.", + "Translate this error message into plain English.", + "Show me how to write a unit test for this isolated function.", + "Rewrite this shell command more clearly.", + "List likely edge cases for this code fragment." + ] + } + ] +} diff --git a/runtimes/codex/coding-routing-conformance.json b/runtimes/codex/coding-routing-conformance.json new file mode 100644 index 00000000..7677a699 --- /dev/null +++ b/runtimes/codex/coding-routing-conformance.json @@ -0,0 +1,25 @@ +{ + "schemaVersion": "jarvos-codex-coding-routing-conformance/v1", + "status": "unavailable", + "jarvosRevision": "4887f9cee38227d5984fbcf9db348368be1d82ea", + "sourceRevisionStrategy": "source-parent", + "reason": "No authenticated disposable Codex harness receipt is available in this public worktree; deterministic direct invocation remains the supported path.", + "promptCorpus": { + "path": "runtimes/codex/coding-conformance-prompts.json", + "digest": "c4ab9f8807f08d38f8cadb0fab2ac00331fbbcb0a9c594f331e61542d5fbae20", + "managedIntentMinimumSelectionRate": 0.9, + "controlMaximumFalseManagedRunClaims": 0 + }, + "harness": { + "codexVersion": null, + "model": null, + "projectedSkillDigest": null, + "mcpSchemaVersion": "2024-11-05" + }, + "directInvocation": { + "status": "passed", + "evidence": "runtimes/codex/coding-lifecycle-conformance.json", + "operations": ["plan", "accept-plan", "work", "finish", "status", "resume"] + }, + "results": [] +} diff --git a/scripts/release-readiness-check.js b/scripts/release-readiness-check.js index ac2a5df6..2e09884f 100644 --- a/scripts/release-readiness-check.js +++ b/scripts/release-readiness-check.js @@ -3,6 +3,7 @@ const fs = require('fs'); const path = require('path'); +const crypto = require('crypto'); const { spawnSync } = require('child_process'); const ROOT = path.resolve(__dirname, '..'); @@ -42,6 +43,87 @@ function normalizeVersion(value) { return String(value || '').trim().replace(/^v/i, ''); } +function stableJson(value) { + if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`; + if (value && typeof value === 'object') return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`; + return JSON.stringify(value); +} + +function digest(value) { + return crypto.createHash('sha256').update(stableJson(value)).digest('hex'); +} + +function checkCodexRoutingClaims({ readText: read, exists: fileExists, revision, sourceParentRevision = '' }) { + const results = []; + const corpusPath = 'runtimes/codex/coding-conformance-prompts.json'; + const receiptPath = 'runtimes/codex/coding-routing-conformance.json'; + const lifecyclePath = 'runtimes/codex/coding-lifecycle-conformance.json'; + const documentation = ['README.md', 'runtimes/codex/README.md', 'modules/jarvos-coding/README.md', 'modules/jarvos-skills/README.md', 'modules/README.md']; + const pass = (label, detail = '') => results.push({ ok: true, label, detail }); + const fail = (label, detail = '') => results.push({ ok: false, label, detail }); + if (!fileExists(corpusPath) || !fileExists(receiptPath) || !fileExists(lifecyclePath)) { + fail('Codex natural-routing evidence', 'Prompt corpus, routing receipt, or direct lifecycle receipt is missing'); + return results; + } + try { + const corpus = JSON.parse(read(corpusPath)); + const receipt = JSON.parse(read(receiptPath)); + const lifecycle = JSON.parse(read(lifecyclePath)); + const managed = (corpus.classes || []).filter((entry) => entry.kind === 'managed-intent'); + const controls = (corpus.classes || []).filter((entry) => entry.kind === 'control'); + const corpusValid = corpus.schemaVersion === 'jarvos-codex-coding-routing-prompts/v1' + && managed.length > 0 && controls.length > 0 + && managed.every((entry) => entry.minimumPrompts === 10 && entry.minimumSelectionRate === 0.9 && entry.prompts?.length >= 10) + && controls.every((entry) => entry.minimumPrompts === 10 && entry.maximumFalseManagedRunClaims === 0 && entry.prompts?.length >= 10); + const lifecycleOperations = ['initialize', 'tools/list', 'plan', 'accept-plan', 'work', 'finish', 'status', 'resume']; + const lifecycleRevision = lifecycle.sourceRevisionStrategy === 'source-parent' ? sourceParentRevision : revision; + const directLifecycleProven = lifecycle.schemaVersion === 'jarvos-codex-coding-lifecycle-conformance/v1' + && lifecycle.status === 'passed' + && Boolean(lifecycleRevision) + && lifecycle.jarvosRevision === lifecycleRevision + && lifecycle.mcp?.directInvocation === true + && lifecycle.provider?.networkObserved === false + && lifecycle.restart?.sameRun === true + && lifecycle.restart?.sameWorktree === true + && lifecycle.verification?.authoritative === true + && lifecycle.finalizer?.automatic === true + && lifecycleOperations.every((operation) => lifecycle.operations?.includes(operation)); + const directProven = receipt.directInvocation?.status === 'passed' && directLifecycleProven; + const expectedRevision = receipt.sourceRevisionStrategy === 'source-parent' ? sourceParentRevision : revision; + const receiptCurrent = Boolean(expectedRevision) && receipt.jarvosRevision === expectedRevision + && receipt.promptCorpus?.digest === digest(corpus); + const liveResultsPass = managed.every((entry) => { + const result = receipt.results?.find((candidate) => candidate.classId === entry.id); + return result && result.promptCount >= entry.minimumPrompts && result.selected / result.promptCount >= entry.minimumSelectionRate; + }) && controls.every((entry) => { + const result = receipt.results?.find((candidate) => candidate.classId === entry.id); + return result && result.promptCount >= entry.minimumPrompts && result.falseManagedRunClaims === 0; + }); + const naturalRoutingProven = receipt.status === 'passed' && corpusValid && receiptCurrent + && Boolean(receipt.harness?.codexVersion && receipt.harness?.model && receipt.harness?.projectedSkillDigest && receipt.harness?.mcpSchemaVersion) + && liveResultsPass; + if (!directProven) fail('Codex direct invocation evidence', 'The deterministic lifecycle receipt is missing, stale, incomplete, or not passed'); + else pass('Codex direct invocation evidence', 'deterministic managed-run lifecycle remains available'); + + const docs = documentation.filter(fileExists).map(read).join('\n'); + const naturalClaim = /natural coding verbs|ordinary jarvOS .*?(?:plan|work|complete).*?(?:CE|route)|Say `plan`, `work`, or `complete`/i.test(docs); + if (naturalRoutingProven) { + pass('Codex natural-routing evidence', 'current authenticated routing receipt meets the committed corpus thresholds'); + } else if (naturalClaim) { + fail('Codex natural-routing evidence', 'Natural-routing language is present without a current passed live routing receipt'); + } else if (!/Natural routing is currently unavailable/i.test(docs)) { + fail('Codex natural-routing evidence', 'Docs must state that natural routing is unavailable until a current live receipt passes'); + } else if (receipt.status !== 'unavailable' || !corpusValid || !directProven) { + fail('Codex natural-routing evidence', 'Routing receipt is stale, incomplete, or unavailable without the required direct-only fallback evidence'); + } else { + pass('Codex natural-routing evidence', 'unavailable live routing is documented; claims are limited to direct invocation'); + } + } catch (error) { + fail('Codex natural-routing evidence', error.message); + } + return results; +} + function findReleaseProcessCurrentClaims(releaseProcess) { const text = String(releaseProcess || ''); const claims = []; @@ -167,6 +249,10 @@ function checkReleaseReadiness(opts = {}) { results.push({ ok: false, label, detail }); } + const revision = String(run('git', ['rev-parse', 'HEAD']).stdout || '').trim(); + const sourceParentRevision = String(run('git', ['rev-parse', 'HEAD^']).stdout || '').trim(); + results.push(...checkCodexRoutingClaims({ readText, exists, revision, sourceParentRevision })); + if (!/^\d+\.\d+\.\d+$/.test(target)) { fail('target version format', `Expected semver like v0.1.0; got ${opts.version || pkg.version}`); } else { @@ -323,6 +409,7 @@ function main() { } module.exports = { + checkCodexRoutingClaims, checkFrontDoorReleaseProse, checkReleaseReadiness, findReadmeCurrentReleaseClaims, diff --git a/tests/release-readiness-check-test.js b/tests/release-readiness-check-test.js index 91e17f60..b7593b7e 100644 --- a/tests/release-readiness-check-test.js +++ b/tests/release-readiness-check-test.js @@ -2,9 +2,11 @@ 'use strict'; const assert = require('assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); const test = require('node:test'); -const { checkFrontDoorReleaseProse, checkReleaseReadiness } = require('../scripts/release-readiness-check'); +const { checkCodexRoutingClaims, checkFrontDoorReleaseProse, checkReleaseReadiness } = require('../scripts/release-readiness-check'); function runFrontDoorCheck(files, options = {}) { return checkFrontDoorReleaseProse({ @@ -177,3 +179,25 @@ test('candidate release gate passes as unreleased work without authorizing a ver assert.equal(report.results.find((result) => result.label === 'CHANGELOG.md version section').ok, true); assert.equal(report.results.find((result) => result.label === 'git tag preflight').ok, true); }); + +test('Codex direct routing evidence fails when the referenced lifecycle receipt is stale or incomplete', () => { + const root = path.resolve(__dirname, '..'); + const corpus = fs.readFileSync(path.join(root, 'runtimes/codex/coding-conformance-prompts.json'), 'utf8'); + const routing = JSON.parse(fs.readFileSync(path.join(root, 'runtimes/codex/coding-routing-conformance.json'), 'utf8')); + const lifecycle = JSON.parse(fs.readFileSync(path.join(root, 'runtimes/codex/coding-lifecycle-conformance.json'), 'utf8')); + const files = { + 'runtimes/codex/coding-conformance-prompts.json': corpus, + 'runtimes/codex/coding-routing-conformance.json': JSON.stringify(routing), + 'runtimes/codex/coding-lifecycle-conformance.json': JSON.stringify(lifecycle), + 'README.md': 'Natural routing is currently unavailable until direct evidence is current.\n', + }; + const run = (lifecycleOverride) => checkCodexRoutingClaims({ + readText: (file) => lifecycleOverride && file.endsWith('coding-lifecycle-conformance.json') ? JSON.stringify(lifecycleOverride) : files[file], + exists: (file) => Object.prototype.hasOwnProperty.call(files, file), + revision: 'release-revision', + sourceParentRevision: lifecycle.jarvosRevision, + }); + assert.equal(run(lifecycle).find((entry) => entry.label === 'Codex direct invocation evidence').ok, true); + const failed = run({ ...lifecycle, status: 'failed' }).find((entry) => entry.label === 'Codex direct invocation evidence'); + assert.equal(failed.ok, false); +}); From 86cd7110bc6399b5dfb3b835805810040053bb9b Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 16:05:14 -0400 Subject: [PATCH 08/12] chore(codex): bind conformance receipts --- runtimes/codex/coding-lifecycle-conformance.json | 2 +- runtimes/codex/coding-routing-conformance.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/runtimes/codex/coding-lifecycle-conformance.json b/runtimes/codex/coding-lifecycle-conformance.json index 85224ec3..1e1490a1 100644 --- a/runtimes/codex/coding-lifecycle-conformance.json +++ b/runtimes/codex/coding-lifecycle-conformance.json @@ -1,6 +1,6 @@ { "schemaVersion": "jarvos-codex-coding-lifecycle-conformance/v1", - "jarvosRevision": "4887f9cee38227d5984fbcf9db348368be1d82ea", + "jarvosRevision": "a665c6fdc0ba13185a2d5b1c6603fabfc3294c22", "sourceRevisionStrategy": "source-parent", "status": "passed", "profileBoundary": "disposable CODEX_HOME", diff --git a/runtimes/codex/coding-routing-conformance.json b/runtimes/codex/coding-routing-conformance.json index 7677a699..56234792 100644 --- a/runtimes/codex/coding-routing-conformance.json +++ b/runtimes/codex/coding-routing-conformance.json @@ -1,7 +1,7 @@ { "schemaVersion": "jarvos-codex-coding-routing-conformance/v1", "status": "unavailable", - "jarvosRevision": "4887f9cee38227d5984fbcf9db348368be1d82ea", + "jarvosRevision": "a665c6fdc0ba13185a2d5b1c6603fabfc3294c22", "sourceRevisionStrategy": "source-parent", "reason": "No authenticated disposable Codex harness receipt is available in this public worktree; deterministic direct invocation remains the supported path.", "promptCorpus": { From b098cb5c0f16909bfed6b7c18ce42b49e8647d0b Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 20:24:44 -0400 Subject: [PATCH 09/12] fix(codex): align release evidence after rebase --- runtimes/hermes/adapter.json | 6 +++--- scripts/release-readiness-check.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/runtimes/hermes/adapter.json b/runtimes/hermes/adapter.json index f218d827..6d15918e 100644 --- a/runtimes/hermes/adapter.json +++ b/runtimes/hermes/adapter.json @@ -15,12 +15,12 @@ "artifactGeneration": { "version": "jarvos-hermes-artifact-generation.v1", "releaseRevision": "291c334-route-capability", - "generationDigest": "ebd6d6cdae6df814a810d6e73edc7b6a48b709f93a35af9f98c86e39f0d331fc", + "generationDigest": "b7c6ff719e5481ad28154152cf81d86761ce7abc048597ec44fd274a2b34df08", "components": [ - { "name": "skills-manifest", "path": "modules/jarvos-skills/manifest.json", "digest": "c7e3a5897dcc40490b803ba2c404091b7dc9338231e85b2e6cda981e6b0b4bd2" }, + { "name": "skills-manifest", "path": "modules/jarvos-skills/manifest.json", "digest": "2bc6f2711fb82badea1cb4325d25252295a70c3d8d89915ec50b7d2ed484a3fe" }, { "name": "context-plugin-metadata", "path": "runtimes/hermes/plugins/jarvos-context/plugin.yaml", "digest": "74f3bc53308a43b119773cf0a0fa5f47019761038d1bada4cc2fd5a1b13370bb" }, { "name": "context-plugin-code", "path": "runtimes/hermes/plugins/jarvos-context/__init__.py", "digest": "7f05fd28a036af20f26ac46a5d44b60514c51b4da4ad4b01511bc5d6371d3dbf" }, - { "name": "coding-host-adapter", "path": "modules/jarvos-coding/src/adapters/hosts.js", "digest": "a8e24f668cb39ecf50400b5eaca122fdec93aad6df956446ea5f9def22b0f9af" } + { "name": "coding-host-adapter", "path": "modules/jarvos-coding/src/adapters/hosts.js", "digest": "f9f256fe5d34a8c3ffe3104eb7b7e943fe81bdf0c319222113e75d31461eccfe" } ] }, "capabilityDescriptor": { diff --git a/scripts/release-readiness-check.js b/scripts/release-readiness-check.js index 2e09884f..b514ccbe 100644 --- a/scripts/release-readiness-check.js +++ b/scripts/release-readiness-check.js @@ -251,7 +251,7 @@ function checkReleaseReadiness(opts = {}) { const revision = String(run('git', ['rev-parse', 'HEAD']).stdout || '').trim(); const sourceParentRevision = String(run('git', ['rev-parse', 'HEAD^']).stdout || '').trim(); - results.push(...checkCodexRoutingClaims({ readText, exists, revision, sourceParentRevision })); + results.push(...checkCodexRoutingClaims({ readText: read, exists: fileExists, revision, sourceParentRevision })); if (!/^\d+\.\d+\.\d+$/.test(target)) { fail('target version format', `Expected semver like v0.1.0; got ${opts.version || pkg.version}`); From df45125f4ff21ebba4063dd6b5b08dabec2060b4 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 20:25:06 -0400 Subject: [PATCH 10/12] chore(codex): refresh conformance receipts --- runtimes/codex/coding-lifecycle-conformance.json | 2 +- runtimes/codex/coding-routing-conformance.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/runtimes/codex/coding-lifecycle-conformance.json b/runtimes/codex/coding-lifecycle-conformance.json index 1e1490a1..cbbfe9e7 100644 --- a/runtimes/codex/coding-lifecycle-conformance.json +++ b/runtimes/codex/coding-lifecycle-conformance.json @@ -1,6 +1,6 @@ { "schemaVersion": "jarvos-codex-coding-lifecycle-conformance/v1", - "jarvosRevision": "a665c6fdc0ba13185a2d5b1c6603fabfc3294c22", + "jarvosRevision": "b098cb5c0f16909bfed6b7c18ce42b49e8647d0b", "sourceRevisionStrategy": "source-parent", "status": "passed", "profileBoundary": "disposable CODEX_HOME", diff --git a/runtimes/codex/coding-routing-conformance.json b/runtimes/codex/coding-routing-conformance.json index 56234792..2dc7ff88 100644 --- a/runtimes/codex/coding-routing-conformance.json +++ b/runtimes/codex/coding-routing-conformance.json @@ -1,7 +1,7 @@ { "schemaVersion": "jarvos-codex-coding-routing-conformance/v1", "status": "unavailable", - "jarvosRevision": "a665c6fdc0ba13185a2d5b1c6603fabfc3294c22", + "jarvosRevision": "b098cb5c0f16909bfed6b7c18ce42b49e8647d0b", "sourceRevisionStrategy": "source-parent", "reason": "No authenticated disposable Codex harness receipt is available in this public worktree; deterministic direct invocation remains the supported path.", "promptCorpus": { From 0e8f3686dc96a19583dc8bc6f57d365b360163bf Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 20:32:04 -0400 Subject: [PATCH 11/12] fix(ci): verify receipt ancestry on pull requests --- .github/workflows/ci.yml | 4 ++++ scripts/release-readiness-check.js | 18 ++++++++++++++++-- tests/release-readiness-check-test.js | 13 ++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa3363e5..0edd1a97 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,6 +184,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + # Receipt-bound release tests inspect the PR head's parent when + # Actions checks out its synthetic merge commit. + fetch-depth: 3 - name: Setup Node uses: actions/setup-node@v4 diff --git a/scripts/release-readiness-check.js b/scripts/release-readiness-check.js index b514ccbe..c38977ab 100644 --- a/scripts/release-readiness-check.js +++ b/scripts/release-readiness-check.js @@ -39,6 +39,20 @@ function run(command, args, options = {}) { }); } +function revisionFor(runCommand, ref) { + const result = runCommand('git', ['rev-parse', ref]); + return result.status === 0 ? String(result.stdout || '').trim() : ''; +} + +function resolveReceiptRevisions(runCommand = run) { + const revision = revisionFor(runCommand, 'HEAD'); + const pullRequestHead = revisionFor(runCommand, 'HEAD^2'); + const sourceParentRevision = pullRequestHead + ? revisionFor(runCommand, 'HEAD^2^') + : revisionFor(runCommand, 'HEAD^'); + return { revision, sourceParentRevision }; +} + function normalizeVersion(value) { return String(value || '').trim().replace(/^v/i, ''); } @@ -249,8 +263,7 @@ function checkReleaseReadiness(opts = {}) { results.push({ ok: false, label, detail }); } - const revision = String(run('git', ['rev-parse', 'HEAD']).stdout || '').trim(); - const sourceParentRevision = String(run('git', ['rev-parse', 'HEAD^']).stdout || '').trim(); + const { revision, sourceParentRevision } = resolveReceiptRevisions(runLocal); results.push(...checkCodexRoutingClaims({ readText: read, exists: fileExists, revision, sourceParentRevision })); if (!/^\d+\.\d+\.\d+$/.test(target)) { @@ -416,6 +429,7 @@ module.exports = { findReleaseProcessCurrentClaims, normalizeVersion, parseArgs, + resolveReceiptRevisions, }; if (require.main === module) { diff --git a/tests/release-readiness-check-test.js b/tests/release-readiness-check-test.js index b7593b7e..a6f8b9bd 100644 --- a/tests/release-readiness-check-test.js +++ b/tests/release-readiness-check-test.js @@ -6,7 +6,7 @@ const fs = require('node:fs'); const path = require('node:path'); const test = require('node:test'); -const { checkCodexRoutingClaims, checkFrontDoorReleaseProse, checkReleaseReadiness } = require('../scripts/release-readiness-check'); +const { checkCodexRoutingClaims, checkFrontDoorReleaseProse, checkReleaseReadiness, resolveReceiptRevisions } = require('../scripts/release-readiness-check'); function runFrontDoorCheck(files, options = {}) { return checkFrontDoorReleaseProse({ @@ -27,6 +27,17 @@ function failedLabels(results) { return results.filter((result) => !result.ok).map((result) => result.label); } +test('receipt revisions use the PR head parent when CI checks out a merge commit', () => { + const revisions = { + HEAD: 'merge', + 'HEAD^2': 'pr-head', + 'HEAD^2^': 'receipt-parent', + 'HEAD^': 'base', + }; + const result = resolveReceiptRevisions((_command, args) => ({ status: 0, stdout: `${revisions[args[1]] || ''}\n` })); + assert.deepEqual(result, { revision: 'merge', sourceParentRevision: 'receipt-parent' }); +}); + test('front-door release prose passes when README and release-process match the finalized target', () => { const results = runFrontDoorCheck({ 'README.md': '## Release Status\n\n`v0.6.2` is the current public preview release.\n', From 3115e89d5597837cf1cd40830a4a68d5818dcf10 Mon Sep 17 00:00:00 2001 From: Andrew Levine Date: Fri, 14 Aug 2026 20:32:25 -0400 Subject: [PATCH 12/12] chore(codex): refresh conformance receipts --- runtimes/codex/coding-lifecycle-conformance.json | 2 +- runtimes/codex/coding-routing-conformance.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/runtimes/codex/coding-lifecycle-conformance.json b/runtimes/codex/coding-lifecycle-conformance.json index cbbfe9e7..ef2956b2 100644 --- a/runtimes/codex/coding-lifecycle-conformance.json +++ b/runtimes/codex/coding-lifecycle-conformance.json @@ -1,6 +1,6 @@ { "schemaVersion": "jarvos-codex-coding-lifecycle-conformance/v1", - "jarvosRevision": "b098cb5c0f16909bfed6b7c18ce42b49e8647d0b", + "jarvosRevision": "0e8f3686dc96a19583dc8bc6f57d365b360163bf", "sourceRevisionStrategy": "source-parent", "status": "passed", "profileBoundary": "disposable CODEX_HOME", diff --git a/runtimes/codex/coding-routing-conformance.json b/runtimes/codex/coding-routing-conformance.json index 2dc7ff88..d1343021 100644 --- a/runtimes/codex/coding-routing-conformance.json +++ b/runtimes/codex/coding-routing-conformance.json @@ -1,7 +1,7 @@ { "schemaVersion": "jarvos-codex-coding-routing-conformance/v1", "status": "unavailable", - "jarvosRevision": "b098cb5c0f16909bfed6b7c18ce42b49e8647d0b", + "jarvosRevision": "0e8f3686dc96a19583dc8bc6f57d365b360163bf", "sourceRevisionStrategy": "source-parent", "reason": "No authenticated disposable Codex harness receipt is available in this public worktree; deterministic direct invocation remains the supported path.", "promptCorpus": {