Job settlement
AURORA A complete job, from mission definition to validator votes and local settlement.
Local chain What to expect Runs actual contracts on a disposable localhost chain using mock tokens and synthetic work. Read the setup guide first.
Guides & runbooks 4
Registered commands 4 Environment setup ↗ Guided tour Inspect the sources Try it locally Architecture Complete library
A CLOSER LOOK
What makes a completed job eligible for payment? For operators and builders learning the contract lifecycle: connect a job specification to work submission, validator decisions and local settlement.
01 Define acceptance Inspect validation.k and validation.n, the challenge window, stake and escrow. Amounts are token base units; the IPFS example URIs are placeholders, not uploaded evidence.
demo/aurora/config/aurora.spec@v2.json ↗ Inspect this source ↓ 02 Follow the contract calls Trace registration, staking, posting, application, submission and validation. This driver executes real contract transactions on the selected local development chain.
demo/aurora/aurora.demo.ts ↗ Inspect this source ↓ 03 Read the receipt The report generator reads run artifacts. Check job IDs, validator votes, settlement transactions and report scope; a generated summary alone does not demonstrate a successful settlement.
demo/aurora/bin/aurora-report.ts ↗ Inspect this source ↓
REAL REPOSITORY MATERIAL
Inspect. Understand. Reproduce. Reading the exact source at revision 5b4cebb3. This browser inspection does not execute the demo.
demo/aurora/config/aurora.spec@v2.json
Select a walkthrough step to explore its source.
Show more fields ↓ Full source text {
"name": "AURORA-Flagship-Job",
"version": "v2",
"domains": ["math", "logic", "commonsense"],
"languages": ["en", "es", "fr"],
"acceptanceCriteriaURI": "ipfs://example-criteria-hash",
"validation": { "k": 2, "n": 3 },
"challengeWindowSec": 86400,
"escrow": { "token": "USDC", "amountPerItem": "5000000" },
"stake": { "worker": "20000000", "validator": "50000000" },
"resultSchema": "ipfs://example-schema-hash",
"notes": "Demo spec; replace URIs with real content-addressed assets."
}
SHA-256 54b3fba38fb2d0c62858b08f3f7c0bc1051aaf997b49ac8ced8c7840749cb3b5
The first source is shown in full. All walkthrough source links and the complete document library remain available without JavaScript.
{"revision":"5b4cebb309a83a7a6749d8911d8bf96a1921e042","sources":[{"file":"demo/aurora/config/aurora.spec@v2.json","content":"{\n \"name\": \"AURORA-Flagship-Job\",\n \"version\": \"v2\",\n \"domains\": [\"math\", \"logic\", \"commonsense\"],\n \"languages\": [\"en\", \"es\", \"fr\"],\n \"acceptanceCriteriaURI\": \"ipfs://example-criteria-hash\",\n \"validation\": { \"k\": 2, \"n\": 3 },\n \"challengeWindowSec\": 86400,\n \"escrow\": { \"token\": \"USDC\", \"amountPerItem\": \"5000000\" },\n \"stake\": { \"worker\": \"20000000\", \"validator\": \"50000000\" },\n \"resultSchema\": \"ipfs://example-schema-hash\",\n \"notes\": \"Demo spec; replace URIs with real content-addressed assets.\"\n}\n","format":"json","sha256":"54b3fba38fb2d0c62858b08f3f7c0bc1051aaf997b49ac8ced8c7840749cb3b5","bytes":508,"download":"/AGIJobsv0/examples/54b3fba38fb2d0c6-aurora.spec_v2.json","source":"https://github.com/MontrealAI/AGIJobsv0/blob/5b4cebb309a83a7a6749d8911d8bf96a1921e042/demo/aurora/config/aurora.spec%40v2.json"},{"file":"demo/aurora/aurora.demo.ts","content":"#!/usr/bin/env ts-node\nimport 'dotenv/config';\nimport fs from 'fs';\nimport path from 'path';\nimport { randomBytes } from 'crypto';\nimport { ethers } from 'ethers';\nimport { resolveNamespace } from './bin/report-paths.cjs';\nimport { quorum as validateQuorum, missionPlan } from './bin/mission-plan.cjs';\n\ntype DeploySummary = {\n contracts: Record<string, string>;\n network?: string;\n governance?: string;\n};\n\ntype Spec = {\n validation: { k: number; n: number };\n escrow?: { amountPerItem?: string };\n stake?: { worker?: string; validator?: string };\n acceptanceCriteriaURI?: string;\n notes?: string;\n};\n\ntype GovernanceAction = {\n target: string;\n method: string;\n txHash: string;\n type: 'forwarded' | 'direct';\n params?: unknown;\n notes?: string;\n before?: Record<string, string>;\n after?: Record<string, string>;\n};\n\ntype ThermostatUpdate = {\n action: string;\n before: string;\n after: string;\n txHash: string;\n};\n\nconst DEFAULT_KEYS = [\n '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80',\n '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d',\n '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a',\n '0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6',\n '0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a',\n];\n\nconst AGIALPHA_CONFIG = JSON.parse(\n fs.readFileSync(path.join('config', 'agialpha.json'), 'utf8')\n);\n\nconst DEFAULT_SPEC_PATH = path.join(\n 'demo',\n 'aurora',\n 'config',\n 'aurora.spec@v2.json'\n);\nconst SPEC_PATH = process.env.AURORA_SPEC_PATH\n ? path.resolve(process.env.AURORA_SPEC_PATH)\n : DEFAULT_SPEC_PATH;\nconst DEFAULT_THERMOSTAT_CONFIG_PATH = path.join(\n 'demo',\n 'aurora',\n 'config',\n 'aurora.thermostat@v2.json'\n);\nconst THERMOSTAT_CONFIG_PATH = process.env.AURORA_THERMOSTAT_CONFIG\n ? path.resolve(process.env.AURORA_THERMOSTAT_CONFIG)\n : DEFAULT_THERMOSTAT_CONFIG_PATH;\nconst MISSION_CONFIG_PATH = process.env.AURORA_MISSION_CONFIG\n ? path.resolve(process.env.AURORA_MISSION_CONFIG)\n : '';\nlet REPORT_SCOPE = process.env.AURORA_REPORT_SCOPE || 'aurora';\n\ntype MissionJob = {\n name: string;\n specPath?: string;\n resultURI?: string;\n agentSubdomain?: string;\n reward?: string;\n workerStake?: string;\n validatorStake?: string;\n deadlineOffsetSec?: number;\n metadata?: Record<string, unknown>;\n notes?: string;\n};\n\nfunction resolveReportNamespace(): string {\n return resolveNamespace(REPORT_SCOPE);\n}\n\nfunction resolveMissionSegments(): string[] {\n const raw = process.env.AURORA_MISSION_LABEL?.trim();\n if (!raw) return [];\n const segments = raw.split(/[\\\\/]+/).filter((segment) => segment.length > 0);\n if (segments.length === 0) return [];\n for (const segment of segments) {\n if (!/^[A-Za-z0-9_.-]+$/.test(segment)) {\n throw new Error(\n `Invalid mission label segment \"${segment}\". Allowed characters: alphanumeric, '-', '_', '.'`\n );\n }\n if (segment === '.' || segment === '..') {\n throw new Error('Mission label segments cannot be relative path tokens.');\n }\n }\n return segments;\n}\n\nfunction resolveSpecPath(): string {\n if (process.env.AURORA_SPEC_PATH) {\n return path.resolve(process.env.AURORA_SPEC_PATH);\n }\n return path.resolve(DEFAULT_SPEC_PATH);\n}\n\nfunction resolveThermostatConfigPath(): string | null {\n if (process.env.AURORA_THERMOSTAT_CONFIG_PATH) {\n return path.resolve(process.env.AURORA_THERMOSTAT_CONFIG_PATH);\n }\n const fallback = path.resolve(DEFAULT_THERMOSTAT_CONFIG_PATH);\n return fs.existsSync(fallback) ? fallback : null;\n}\n\ntype ThermostatConfig = {\n systemTemperature?: string | number;\n temperatureBounds?: { min: string | number; max: string | number };\n integralBounds?: { min: string | number; max: string | number };\n pid?: { kp: string | number; ki: string | number; kd: string | number };\n kpiWeights?: {\n emission: string | number;\n backlog: string | number;\n sla: string | number;\n };\n roleTemperatures?: Record<string, string | number>;\n unsetRoleTemperatures?: string[];\n};\n\ntype MissionConfig = {\n version?: string;\n scope?: string;\n jobs: MissionJob[];\n description?: string;\n};\n\nconst THERMOSTAT_ROLE_ALIAS: Record<string, number> = {\n agent: 0,\n validator: 1,\n operator: 2,\n employer: 3,\n};\n\nfunction parseNetworkArg(): string {\n const idx = process.argv.indexOf('--network');\n if (idx !== -1 && process.argv[idx + 1]) {\n return process.argv[idx + 1];\n }\n if (process.env.NETWORK) {\n return process.env.NETWORK;\n }\n return 'localhost';\n}\n\nfunction readJsonFile<T>(filePath: string): T {\n const absolute = path.resolve(filePath);\n if (!fs.existsSync(absolute)) {\n throw new Error(`Required file not found: ${absolute}`);\n }\n try {\n return JSON.parse(fs.readFileSync(absolute, 'utf8')) as T;\n } catch (err) {\n throw new Error(\n `Unable to parse JSON at ${absolute}: ${(err as Error).message}`\n );\n }\n}\n\nfunction slugify(value: string, fallback: string): string {\n const base = value\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+/, '')\n .replace(/-+$/, '');\n return base || fallback;\n}\n\nfunction resolveReportBaseDir(net: string): string {\n const namespace = resolveReportNamespace();\n const missionSegments = resolveMissionSegments();\n const parts = ['reports', net, namespace, ...missionSegments];\n return path.join(...parts);\n}\n\nfunction writeReceipt(net: string, name: string, data: unknown) {\n const baseDir = resolveReportBaseDir(net);\n const dir = path.join(baseDir, 'receipts');\n const primaryPath = path.join(dir, name);\n fs.mkdirSync(path.dirname(primaryPath), { recursive: true });\n fs.writeFileSync(primaryPath, JSON.stringify(data, null, 2));\n const legacyDir = path.join('reports', net, REPORT_SCOPE, 'receipts');\n const outputPath = path.join(legacyDir, name);\n fs.mkdirSync(path.dirname(outputPath), { recursive: true });\n fs.writeFileSync(outputPath, JSON.stringify(data, null, 2));\n}\n\nfunction resolveDeploySummaryPath(net: string): string {\n if (process.env.AURORA_DEPLOY_OUTPUT) {\n return path.resolve(process.env.AURORA_DEPLOY_OUTPUT);\n }\n const namespace = resolveReportNamespace();\n return path.resolve('reports', net, namespace, 'receipts', 'deploy.json');\n}\n\nfunction specAmountToWei(amount: string | undefined, decimals: number): bigint {\n if (!amount) return 0n;\n const cleaned = amount.trim();\n if (!cleaned) return 0n;\n const base = BigInt(cleaned);\n if (decimals <= 6) {\n const scale = BigInt(10) ** BigInt(decimals);\n return base * scale;\n }\n const scale = BigInt(10) ** BigInt(decimals - 6);\n return base * scale;\n}\n\nfunction formatUnits(value: bigint, decimals: number): string {\n return ethers.formatUnits(value, decimals);\n}\n\nfunction parseSigned(value: string | number | bigint, label: string): bigint {\n if (typeof value === 'bigint') {\n return value;\n }\n if (typeof value === 'number') {\n if (!Number.isFinite(value)) {\n throw new Error(`Invalid number for ${label}: ${value}`);\n }\n return BigInt(Math.trunc(value));\n }\n const trimmed = value.trim();\n if (trimmed.length === 0) {\n throw new Error(`Missing value for ${label}`);\n }\n const negative = trimmed.startsWith('-');\n const digits = negative ? trimmed.slice(1) : trimmed;\n if (!/^\\d+$/.test(digits)) {\n throw new Error(`Invalid integer for ${label}: ${value}`);\n }\n const parsed = BigInt(digits);\n return negative ? -parsed : parsed;\n}\n\nfunction normaliseArg(value: unknown): unknown {\n if (typeof value === 'bigint') {\n return value.toString();\n }\n if (Array.isArray(value)) {\n return value.map((item) => normaliseArg(item));\n }\n if (typeof value === 'object' && value !== null) {\n const entries = Object.entries(value as Record<string, unknown>).map(\n ([k, v]) => [k, normaliseArg(v)]\n );\n return Object.fromEntries(entries);\n }\n return value;\n}\n\ntype AddressedSigner = ethers.Signer & { address: string };\n\nfunction bufferGasEstimates(signer: ethers.Signer): void {\n const estimateGas = signer.estimateGas.bind(signer);\n // Identity-cache writes can change between estimation and the mined block.\n // Keep RPC estimation (including revert checks), with 20% execution headroom.\n signer.estimateGas = async (tx) =>\n ((await estimateGas(tx)) * 120n + 99n) / 100n;\n}\n\nfunction createNonceManagedSigner(\n provider: ethers.JsonRpcProvider,\n privateKey: string\n): AddressedSigner {\n const wallet = new ethers.Wallet(privateKey, provider);\n bufferGasEstimates(wallet);\n const manager = new ethers.NonceManager(wallet);\n return Object.assign(manager, { address: wallet.address }) as AddressedSigner;\n}\n\nasync function impersonateSigner(\n provider: ethers.JsonRpcProvider,\n address: string\n): Promise<AddressedSigner> {\n const normalised = ethers.getAddress(address);\n const methods = ['hardhat_impersonateAccount', 'anvil_impersonateAccount'];\n let impersonated = false;\n for (const method of methods) {\n try {\n await provider.send(method, [normalised]);\n impersonated = true;\n break;\n } catch (err) {\n // Continue trying other RPC namespaces.\n }\n }\n if (!impersonated) {\n throw new Error(\n `Unable to impersonate required account ${normalised}. Provide a PRIVATE_KEY with control or enable impersonation.`\n );\n }\n let signer;\n try {\n signer = await provider.getSigner(normalised);\n } catch (err) {\n throw new Error(\n `Provider cannot supply signer for ${normalised}: ${\n (err as Error).message\n }`\n );\n }\n bufferGasEstimates(signer);\n const manager = new ethers.NonceManager(signer);\n return Object.assign(manager, { address: normalised }) as AddressedSigner;\n}\n\nasync function stopImpersonating(\n provider: ethers.JsonRpcProvider,\n address: string\n) {\n const normalised = ethers.getAddress(address);\n const methods = [\n 'hardhat_stopImpersonatingAccount',\n 'anvil_stopImpersonatingAccount',\n ];\n for (const method of methods) {\n try {\n await provider.send(method, [normalised]);\n } catch (err) {\n // Ignore errors so cleanup is best-effort.\n }\n }\n}\n\nasync function ensureAgialpha(\n provider: ethers.JsonRpcProvider,\n owner: AddressedSigner\n) {\n const tokenAddress = ethers.getAddress(AGIALPHA_CONFIG.address);\n const code = await provider.getCode(tokenAddress);\n const artifactPath = path.join(\n 'artifacts',\n 'contracts',\n 'test',\n 'AGIALPHAToken.sol',\n 'AGIALPHAToken.json'\n );\n if (!fs.existsSync(artifactPath)) {\n throw new Error(\n 'Missing AGIALPHAToken artifact. Run `npx hardhat compile` first.'\n );\n }\n const artifact = JSON.parse(fs.readFileSync(artifactPath, 'utf8')) as {\n abi: any;\n deployedBytecode: string;\n };\n\n if (code === '0x') {\n const network = await provider.getNetwork();\n if (network.chainId !== 31337n) {\n throw new Error(\n `AGIALPHA token not deployed at ${tokenAddress} on chain ${network.chainId}`\n );\n }\n await provider.send('hardhat_setCode', [\n tokenAddress,\n artifact.deployedBytecode,\n ]);\n const ownerSlot = ethers.toBeHex(5, 32);\n const ownerValue = ethers.zeroPadValue(await owner.getAddress(), 32);\n await provider.send('hardhat_setStorageAt', [\n tokenAddress,\n ownerSlot,\n ownerValue,\n ]);\n }\n\n return new ethers.Contract(tokenAddress, artifact.abi, owner);\n}\n\nasync function executeGovernanceCall(\n pause: ethers.Contract,\n target: string,\n iface: ethers.Interface,\n method: string,\n args: unknown[]\n) {\n const data = iface.encodeFunctionData(method, args);\n const tx = await pause.executeGovernanceCall(target, data);\n const receipt = await tx.wait();\n return receipt?.hash || tx.hash;\n}\n\nfunction deriveCommitPlan(\n jobId: bigint,\n approve: boolean,\n validator: string,\n nonce: bigint,\n specHash: string,\n chainId: bigint,\n domainSeparator: string\n) {\n const abi = ethers.AbiCoder.defaultAbiCoder();\n const burnTxHash = ethers.ZeroHash;\n const salt = ethers.hexlify(randomBytes(32));\n const outcomeHash = ethers.keccak256(\n abi.encode(\n ['uint256', 'bytes32', 'bool', 'bytes32'],\n [nonce, specHash, approve, burnTxHash]\n )\n );\n const commitHash = ethers.keccak256(\n abi.encode(\n ['uint256', 'bytes32', 'bytes32', 'address', 'uint256', 'bytes32'],\n [jobId, outcomeHash, salt, validator, chainId, domainSeparator]\n )\n );\n return { commitHash, salt, burnTxHash };\n}\n\nasync function advanceTime(\n provider: ethers.JsonRpcProvider,\n seconds: number\n): Promise<'none' | 'warp' | 'wait'> {\n const clamped = Math.max(0, Math.trunc(seconds));\n if (clamped <= 0) {\n return 'none';\n }\n try {\n await provider.send('evm_increaseTime', [clamped]);\n await provider.send('evm_mine', []);\n return 'warp';\n } catch (err) {\n await new Promise((resolve) => setTimeout(resolve, clamped * 1000));\n return 'wait';\n }\n}\n\nasync function applyThermostatConfig(\n thermostat: ethers.Contract,\n thermostatInterface: ethers.Interface,\n recordCall: (\n targetName: string,\n targetAddress: string,\n iface: ethers.Interface,\n method: string,\n args: unknown[],\n options?: {\n notes?: string;\n before?: Record<string, string>;\n after?: Record<string, string>;\n }\n ) => Promise<string>,\n config: ThermostatConfig\n): Promise<ThermostatUpdate[]> {\n const updates: ThermostatUpdate[] = [];\n const targetAddress = await thermostat.getAddress();\n\n const pushUpdate = (\n action: string,\n before: bigint,\n after: bigint,\n txHash: string,\n note?: string\n ) => {\n updates.push({\n action: note ? `${action} (${note})` : action,\n before: before.toString(),\n after: after.toString(),\n txHash,\n });\n };\n\n if (config.temperatureBounds) {\n const beforeMin = await thermostat.minTemp();\n const beforeMax = await thermostat.maxTemp();\n const min = parseSigned(\n config.temperatureBounds.min,\n 'temperatureBounds.min'\n );\n const max = parseSigned(\n config.temperatureBounds.max,\n 'temperatureBounds.max'\n );\n const txHash = await recordCall(\n 'Thermostat',\n targetAddress,\n thermostatInterface,\n 'setTemperatureBounds',\n [min, max],\n {\n notes: 'Adjust system temperature bounds',\n before: {\n min: beforeMin.toString(),\n max: beforeMax.toString(),\n },\n after: { min: min.toString(), max: max.toString() },\n }\n );\n const afterMin = await thermostat.minTemp();\n const afterMax = await thermostat.maxTemp();\n pushUpdate('setTemperatureBounds:min', beforeMin, afterMin, txHash);\n pushUpdate('setTemperatureBounds:max', beforeMax, afterMax, txHash);\n }\n\n if (config.integralBounds) {\n const beforeMin = await thermostat.integralMin();\n const beforeMax = await thermostat.integralMax();\n const min = parseSigned(config.integralBounds.min, 'integralBounds.min');\n const max = parseSigned(config.integralBounds.max, 'integralBounds.max');\n const txHash = await recordCall(\n 'Thermostat',\n targetAddress,\n thermostatInterface,\n 'setIntegralBounds',\n [min, max],\n {\n notes: 'Tune integral bounds for controller stability',\n before: {\n min: beforeMin.toString(),\n max: beforeMax.toString(),\n },\n after: { min: min.toString(), max: max.toString() },\n }\n );\n const afterMin = await thermostat.integralMin();\n const afterMax = await thermostat.integralMax();\n pushUpdate('setIntegralBounds:min', beforeMin, afterMin, txHash);\n pushUpdate('setIntegralBounds:max', beforeMax, afterMax, txHash);\n }\n if (config.pid) {\n const beforeKp = await thermostat.kp();\n const beforeKi = await thermostat.ki();\n const beforeKd = await thermostat.kd();\n const kp = parseSigned(config.pid.kp, 'pid.kp');\n const ki = parseSigned(config.pid.ki, 'pid.ki');\n const kd = parseSigned(config.pid.kd, 'pid.kd');\n const txHash = await recordCall(\n 'Thermostat',\n targetAddress,\n thermostatInterface,\n 'setPID',\n [kp, ki, kd],\n {\n notes: 'Update PID controller coefficients',\n before: {\n kp: beforeKp.toString(),\n ki: beforeKi.toString(),\n kd: beforeKd.toString(),\n },\n after: { kp: kp.toString(), ki: ki.toString(), kd: kd.toString() },\n }\n );\n const afterKp = await thermostat.kp();\n const afterKi = await thermostat.ki();\n const afterKd = await thermostat.kd();\n pushUpdate('setPID:kp', beforeKp, afterKp, txHash);\n pushUpdate('setPID:ki', beforeKi, afterKi, txHash);\n pushUpdate('setPID:kd', beforeKd, afterKd, txHash);\n }\n\n if (config.kpiWeights) {\n const beforeEmission = await thermostat.wEmission();\n const beforeBacklog = await thermostat.wBacklog();\n const beforeSla = await thermostat.wSla();\n const emission = parseSigned(\n config.kpiWeights.emission,\n 'kpiWeights.emission'\n );\n const backlog = parseSigned(\n config.kpiWeights.backlog,\n 'kpiWeights.backlog'\n );\n const sla = parseSigned(config.kpiWeights.sla, 'kpiWeights.sla');\n const txHash = await recordCall(\n 'Thermostat',\n targetAddress,\n thermostatInterface,\n 'setKPIWeights',\n [emission, backlog, sla],\n {\n notes: 'Rebalance KPI weights for economic planning',\n before: {\n emission: beforeEmission.toString(),\n backlog: beforeBacklog.toString(),\n sla: beforeSla.toString(),\n },\n after: {\n emission: emission.toString(),\n backlog: backlog.toString(),\n sla: sla.toString(),\n },\n }\n );\n const afterEmission = await thermostat.wEmission();\n const afterBacklog = await thermostat.wBacklog();\n const afterSla = await thermostat.wSla();\n pushUpdate('setKPIWeights:emission', beforeEmission, afterEmission, txHash);\n pushUpdate('setKPIWeights:backlog', beforeBacklog, afterBacklog, txHash);\n pushUpdate('setKPIWeights:sla', beforeSla, afterSla, txHash);\n }\n\n if (config.systemTemperature !== undefined) {\n const before = await thermostat.systemTemperature();\n const value = parseSigned(config.systemTemperature, 'systemTemperature');\n const txHash = await recordCall(\n 'Thermostat',\n targetAddress,\n thermostatInterface,\n 'setSystemTemperature',\n [value],\n {\n notes: 'Dial global temperature for agent incentives',\n before: { temperature: before.toString() },\n after: { temperature: value.toString() },\n }\n );\n const after = await thermostat.systemTemperature();\n pushUpdate('setSystemTemperature', before, after, txHash);\n }\n\n if (config.roleTemperatures) {\n for (const [roleLabel, temp] of Object.entries(config.roleTemperatures)) {\n const key = roleLabel.trim().toLowerCase();\n const roleId = THERMOSTAT_ROLE_ALIAS[key];\n if (roleId === undefined) {\n throw new Error(`Unknown thermostat role: ${roleLabel}`);\n }\n const before = await thermostat.getRoleTemperature(roleId);\n const value = parseSigned(temp, `roleTemperatures.${roleLabel}`);\n const txHash = await recordCall(\n 'Thermostat',\n targetAddress,\n thermostatInterface,\n 'setRoleTemperature',\n [roleId, value],\n {\n notes: `Set role temperature for ${roleLabel}`,\n before: { temperature: before.toString() },\n after: { temperature: value.toString() },\n }\n );\n const after = await thermostat.getRoleTemperature(roleId);\n pushUpdate(`setRoleTemperature:${roleLabel}`, before, after, txHash);\n }\n }\n\n if (config.unsetRoleTemperatures) {\n for (const roleLabel of config.unsetRoleTemperatures) {\n const key = roleLabel.trim().toLowerCase();\n if (!key) continue;\n const roleId = THERMOSTAT_ROLE_ALIAS[key];\n if (roleId === undefined) {\n throw new Error(`Unknown thermostat role: ${roleLabel}`);\n }\n const before = await thermostat.getRoleTemperature(roleId);\n const txHash = await recordCall(\n 'Thermostat',\n targetAddress,\n thermostatInterface,\n 'unsetRoleTemperature',\n [roleId],\n {\n notes: `Unset role temperature for ${roleLabel}`,\n before: { temperature: before.toString() },\n after: { temperature: '0' },\n }\n );\n const after = await thermostat.getRoleTemperature(roleId);\n pushUpdate(`unsetRoleTemperature:${roleLabel}`, before, after, txHash);\n }\n }\n\n return updates;\n}\n\nasync function main() {\n const networkName = parseNetworkArg();\n const rpcUrl = process.env.RPC_URL || 'http://127.0.0.1:8545';\n // Instant local blocks can invalidate ethers' 250 ms nonce cache between\n // consecutive transactions, including the wrapped signer's gas estimate.\n const provider = new ethers.JsonRpcProvider(rpcUrl, undefined, {\n cacheTimeout: -1,\n });\n const chain = await provider.getNetwork();\n if (chain.chainId === 31337n) provider.pollingInterval = 100;\n const decimals = Number(AGIALPHA_CONFIG.decimals || 18);\n\n const governanceActions: GovernanceAction[] = [];\n\n const employerKey = process.env.PRIVATE_KEY || DEFAULT_KEYS[0];\n const workerKey = process.env.AURORA_WORKER_KEY || DEFAULT_KEYS[1];\n const validatorKeys = [\n process.env.AURORA_VALIDATOR1_KEY || DEFAULT_KEYS[2],\n process.env.AURORA_VALIDATOR2_KEY || DEFAULT_KEYS[3],\n process.env.AURORA_VALIDATOR3_KEY || DEFAULT_KEYS[4],\n ...[5, 6].map(\n (index) =>\n process.env[`AURORA_VALIDATOR${index - 1}_KEY`] ||\n (chain.chainId === 31337n\n ? ethers.HDNodeWallet.fromPhrase(\n 'test test test test test test test test test test test junk',\n undefined,\n `m/44'/60'/0'/0/${index}`\n ).privateKey\n : undefined)\n ),\n ];\n\n const employer = createNonceManagedSigner(provider, employerKey);\n const worker = createNonceManagedSigner(provider, workerKey);\n\n const specPath = resolveSpecPath();\n const spec = readJsonFile<Spec>(specPath);\n if (!spec.validation || !spec.validation.k || !spec.validation.n) {\n throw new Error('Validation quorum (k-of-n) must be defined in the spec.');\n }\n const thermostatConfigPath = resolveThermostatConfigPath();\n const thermostatConfig =\n thermostatConfigPath !== null\n ? readJsonFile<ThermostatConfig>(thermostatConfigPath)\n : null;\n const baseSpec = readJsonFile<Spec>(SPEC_PATH);\n if (\n !baseSpec.validation ||\n !baseSpec.validation.k ||\n !baseSpec.validation.n\n ) {\n throw new Error('Validation quorum (k-of-n) must be defined in the spec.');\n }\n\n if (MISSION_CONFIG_PATH) missionPlan(MISSION_CONFIG_PATH);\n const missionConfig = MISSION_CONFIG_PATH\n ? readJsonFile<MissionConfig>(MISSION_CONFIG_PATH)\n : null;\n if (missionConfig?.scope && !process.env.AURORA_REPORT_SCOPE) {\n REPORT_SCOPE = missionConfig.scope;\n }\n\n const missionJobs =\n missionConfig &&\n Array.isArray(missionConfig.jobs) &&\n missionConfig.jobs.length > 0\n ? missionConfig.jobs\n : [\n {\n name: baseSpec.name || 'AURORA-Flagship-Job',\n specPath: SPEC_PATH,\n resultURI: 'ipfs://aurora-demo-result',\n agentSubdomain: 'aurora-agent',\n reward: baseSpec.escrow?.amountPerItem,\n workerStake: baseSpec.stake?.worker,\n validatorStake: baseSpec.stake?.validator,\n deadlineOffsetSec: 3600,\n },\n ];\n\n const resolvedJobs = missionJobs.map((job, idx) => {\n const specPath = job.specPath ? path.resolve(job.specPath) : SPEC_PATH;\n const jobSpec = readJsonFile<Spec>(specPath);\n validateQuorum(jobSpec.validation, job.name);\n if (!jobSpec.validation || !jobSpec.validation.k || !jobSpec.validation.n) {\n throw new Error(\n `Validation quorum (k-of-n) must be defined in spec for mission job ${job.name}.`\n );\n }\n const rewardAmount =\n specAmountToWei(job.reward, decimals) ||\n specAmountToWei(jobSpec.escrow?.amountPerItem, decimals) ||\n ethers.parseUnits('5', decimals);\n const workerStakeAmount =\n specAmountToWei(job.workerStake, decimals) ||\n specAmountToWei(jobSpec.stake?.worker, decimals) ||\n ethers.parseUnits('20', decimals);\n const validatorStakeAmount =\n specAmountToWei(job.validatorStake, decimals) ||\n specAmountToWei(jobSpec.stake?.validator, decimals) ||\n ethers.parseUnits('50', decimals);\n const deadlineOffset = job.deadlineOffsetSec || 3600;\n const slug = slugify(job.name, `job-${idx + 1}`);\n return {\n name: job.name,\n slug,\n spec: jobSpec,\n specPath,\n rewardAmount,\n workerStakeAmount,\n validatorStakeAmount,\n resultUri: job.resultURI || `ipfs://aurora-demo-result-${slug}`,\n agentSubdomain: job.agentSubdomain || slug.replace(/[^a-z0-9]/g, ''),\n deadlineOffset,\n metadata: job.metadata || {},\n notes: job.notes,\n };\n });\n\n if (resolvedJobs.length === 0) {\n throw new Error('Mission must include at least one job.');\n }\n\n const referenceValidation = resolvedJobs[0].spec.validation;\n const validatorCount = Math.max(\n ...resolvedJobs.map((job) => job.spec.validation.n)\n );\n const quorum = referenceValidation.k;\n\n const selectedValidatorKeys = validatorKeys.slice(0, validatorCount);\n if (\n selectedValidatorKeys.length < validatorCount ||\n selectedValidatorKeys.some((key) => !key)\n ) {\n throw new Error(\n 'Insufficient validator keys configured for the selected quorum.'\n );\n }\n const validators = selectedValidatorKeys.map((key) =>\n createNonceManagedSigner(provider, key!)\n );\n\n const agentRole = 0;\n const validatorRole = 1;\n const platformRole = 2;\n\n const summaryPath = resolveDeploySummaryPath(networkName);\n const deploySummary = readJsonFile<DeploySummary>(summaryPath);\n if (!deploySummary.contracts) {\n throw new Error(`Deployment summary missing contracts map: ${summaryPath}`);\n }\n\n const addresses = deploySummary.contracts;\n const thermostatAddress = addresses.Thermostat;\n if (thermostatConfig && !thermostatAddress) {\n console.warn(\n '⚠️ Thermostat config provided but deployment summary lacks Thermostat address. Skipping thermostat adjustments.'\n );\n }\n\n const loadArtifact = (name: string) =>\n JSON.parse(\n fs.readFileSync(\n path.join(\n 'artifacts',\n 'contracts',\n 'v2',\n `${name}.sol`,\n `${name}.json`\n ),\n 'utf8'\n )\n );\n\n const jobRegistryArtifact = loadArtifact('JobRegistry');\n const stakeManagerArtifact = loadArtifact('StakeManager');\n const validationModuleArtifact = loadArtifact('ValidationModule');\n const identityRegistryArtifact = loadArtifact('IdentityRegistry');\n const systemPauseArtifact = loadArtifact('SystemPause');\n const thermostatArtifact = thermostatAddress\n ? loadArtifact('Thermostat')\n : null;\n\n const jobRegistry = new ethers.Contract(\n addresses.JobRegistry,\n jobRegistryArtifact.abi,\n employer\n );\n const stakeManager = new ethers.Contract(\n addresses.StakeManager,\n stakeManagerArtifact.abi,\n employer\n );\n const validationModule = new ethers.Contract(\n addresses.ValidationModule,\n validationModuleArtifact.abi,\n employer\n );\n const identityRegistry = new ethers.Contract(\n addresses.IdentityRegistry,\n identityRegistryArtifact.abi,\n employer\n );\n const systemPause = new ethers.Contract(\n addresses.SystemPause,\n systemPauseArtifact.abi,\n employer\n );\n const thermostat =\n thermostatArtifact && thermostatAddress\n ? new ethers.Contract(thermostatAddress, thermostatArtifact.abi, employer)\n : null;\n\n const recordForwardGovernanceCall = async (\n targetName: string,\n targetAddress: string,\n iface: ethers.Interface,\n method: string,\n args: unknown[],\n options?: {\n notes?: string;\n before?: Record<string, string>;\n after?: Record<string, string>;\n }\n ) => {\n const txHash = await executeGovernanceCall(\n systemPause,\n targetAddress,\n iface,\n method,\n args\n );\n governanceActions.push({\n target: targetName,\n method,\n txHash,\n type: 'forwarded',\n params: normaliseArg(args),\n notes: options?.notes,\n before: options?.before,\n after: options?.after,\n });\n return txHash;\n };\n\n const recordDirectGovernanceCall = async (\n targetName: string,\n method: string,\n action: () => Promise<ethers.ContractTransactionResponse>,\n notes?: string\n ) => {\n const tx = await action();\n const receipt = await tx.wait();\n const txHash = receipt?.hash || tx.hash;\n governanceActions.push({\n target: targetName,\n method,\n txHash,\n type: 'direct',\n notes,\n });\n return txHash;\n };\n\n const token = await ensureAgialpha(provider, employer);\n\n const baselineMint = ethers.parseUnits('1000', decimals);\n const totalReward = resolvedJobs.reduce(\n (acc, job) => acc + job.rewardAmount,\n 0n\n );\n const maxReward = resolvedJobs.reduce(\n (acc, job) => (job.rewardAmount > acc ? job.rewardAmount : acc),\n 0n\n );\n const maxWorkerStake = resolvedJobs.reduce(\n (acc, job) => (job.workerStakeAmount > acc ? job.workerStakeAmount : acc),\n 0n\n );\n const maxValidatorStake = resolvedJobs.reduce(\n (acc, job) =>\n job.validatorStakeAmount > acc ? job.validatorStakeAmount : acc,\n 0n\n );\n const computedMint = [\n baselineMint,\n totalReward + maxWorkerStake,\n maxWorkerStake + maxValidatorStake,\n ].reduce((acc, value) => (value > acc ? value : acc));\n const mintAmount = computedMint;\n\n const participants = [employer, worker, ...validators];\n for (const wallet of participants) {\n const bal = await token.balanceOf(wallet.address);\n if (bal < mintAmount) {\n const tx = await token.mint(wallet.address, mintAmount - bal);\n await tx.wait();\n }\n const allowance = await token.allowance(\n wallet.address,\n addresses.StakeManager\n );\n const requiredAllowance =\n wallet === employer ? mintAmount + totalReward : mintAmount;\n if (allowance < requiredAllowance) {\n const approveTx = await token\n .connect(wallet)\n .approve(addresses.StakeManager, ethers.MaxUint256);\n await approveTx.wait();\n }\n }\n\n await recordDirectGovernanceCall(\n 'SystemPause',\n 'pauseAll',\n () => systemPause.pauseAll(),\n 'Emergency drill: pause every core module'\n );\n await recordDirectGovernanceCall(\n 'SystemPause',\n 'unpauseAll',\n () => systemPause.unpauseAll(),\n 'Resume operations after pause drill'\n );\n\n const originalAgentMinimum = await stakeManager.roleMinimumStake(agentRole);\n const originalValidatorMinimum = await stakeManager.roleMinimumStake(\n validatorRole\n );\n const originalPlatformMinimum = await stakeManager.roleMinimumStake(\n platformRole\n );\n const stakeMinimumBaseline = {\n agent: originalAgentMinimum,\n validator: originalValidatorMinimum,\n platform: originalPlatformMinimum,\n };\n\n const adjustedAgentMinimum =\n maxWorkerStake / 2n > 0n ? maxWorkerStake / 2n : 1n;\n const adjustedValidatorMinimum =\n maxValidatorStake / 2n > 0n ? maxValidatorStake / 2n : 1n;\n const adjustedPlatformMinimum =\n maxValidatorStake / 4n > 0n ? maxValidatorStake / 4n : 1n;\n const stakeMinimumAdjusted = {\n agent: adjustedAgentMinimum,\n validator: adjustedValidatorMinimum,\n platform: adjustedPlatformMinimum,\n };\n\n await recordForwardGovernanceCall(\n 'StakeManager',\n addresses.StakeManager,\n stakeManager.interface,\n 'setRoleMinimums',\n [adjustedAgentMinimum, adjustedValidatorMinimum, adjustedPlatformMinimum],\n {\n notes: 'Lower minimum stakes so demo identities can onboard quickly',\n before: {\n agent: formatUnits(stakeMinimumBaseline.agent, decimals),\n validator: formatUnits(stakeMinimumBaseline.validator, decimals),\n platform: formatUnits(stakeMinimumBaseline.platform, decimals),\n },\n after: {\n agent: formatUnits(stakeMinimumAdjusted.agent, decimals),\n validator: formatUnits(stakeMinimumAdjusted.validator, decimals),\n platform: formatUnits(stakeMinimumAdjusted.platform, decimals),\n },\n }\n );\n\n const originalJobStake = await jobRegistry.jobStake();\n const fallbackJobStake = maxReward / 10n > 0n ? maxReward / 10n : 1n;\n const adjustedJobStake =\n fallbackJobStake > maxWorkerStake ? maxWorkerStake : fallbackJobStake;\n\n await recordForwardGovernanceCall(\n 'JobRegistry',\n addresses.JobRegistry,\n jobRegistry.interface,\n 'setJobStake',\n [adjustedJobStake],\n {\n notes: 'Set worker collateral within the funded demo stake',\n before: { stake: formatUnits(originalJobStake, decimals) },\n after: { stake: formatUnits(adjustedJobStake, decimals) },\n }\n );\n await recordForwardGovernanceCall(\n 'JobRegistry',\n addresses.JobRegistry,\n jobRegistry.interface,\n 'setAcknowledger',\n [addresses.StakeManager, true],\n {\n notes:\n 'Allow StakeManager to acknowledge tax policy on behalf of participants',\n }\n );\n const stakeManagerAcknowledger = await jobRegistry.acknowledgers(\n addresses.StakeManager\n );\n if (!stakeManagerAcknowledger) {\n throw new Error('StakeManager is not registered as a tax acknowledger.');\n }\n\n let identityCleanup: (() => Promise<void>) | null = null;\n let identityReady = true;\n try {\n const pendingIdentityOwner = await identityRegistry.pendingOwner();\n if (pendingIdentityOwner.toLowerCase() === employer.address.toLowerCase()) {\n await recordDirectGovernanceCall(\n 'IdentityRegistry',\n 'acceptOwnership',\n () => identityRegistry.acceptOwnership(),\n 'Accept the staged two-step handoff to the configured governance signer'\n );\n }\n const identityOwnerAddress = await identityRegistry.owner();\n const normalisedIdentityOwner = identityOwnerAddress\n ? identityOwnerAddress.toLowerCase()\n : ethers.ZeroAddress;\n let identityOwnerSigner: AddressedSigner = employer;\n if (normalisedIdentityOwner !== employer.address.toLowerCase()) {\n const ownerCode = await provider.getCode(identityOwnerAddress);\n if (ownerCode !== '0x') {\n throw new Error(\n `Identity owner ${identityOwnerAddress} is a contract. Skipping manual allowlist; configure ENS proofs instead.`\n );\n }\n identityOwnerSigner = await impersonateSigner(\n provider,\n identityOwnerAddress\n );\n const balance = await provider.getBalance(identityOwnerAddress);\n const minimumBalance = ethers.parseEther('0.1');\n if (balance < minimumBalance) {\n const fundTx = await employer.sendTransaction({\n to: identityOwnerAddress,\n value: minimumBalance,\n });\n await fundTx.wait();\n }\n identityCleanup = () => stopImpersonating(provider, identityOwnerAddress);\n }\n\n const identityWithOwner = identityRegistry.connect(identityOwnerSigner);\n await recordDirectGovernanceCall(\n 'IdentityRegistry',\n 'addAdditionalAgent',\n () => identityWithOwner.addAdditionalAgent(worker.address),\n 'Allow flagship worker to onboard without ENS proof'\n );\n for (const validator of validators) {\n await recordDirectGovernanceCall(\n 'IdentityRegistry',\n 'addAdditionalValidator',\n () => identityWithOwner.addAdditionalValidator(validator.address),\n 'Whitelist validator for flagship mission quorum'\n );\n }\n } catch (err) {\n identityReady = false;\n console.warn(\n '⚠️ Unable to apply identity overrides automatically:',\n (err as Error).message\n );\n } finally {\n if (identityCleanup) {\n await identityCleanup();\n }\n }\n\n if (!identityReady) {\n writeReceipt(networkName, 'mission.json', {\n scope: REPORT_SCOPE,\n version: missionConfig?.version || '1.0',\n description: missionConfig?.description,\n jobs: [],\n });\n throw new Error(\n 'Mission execution blocked: identity overrides could not be applied.'\n );\n }\n\n const validationInterface = new ethers.Interface(\n validationModuleArtifact.abi\n );\n const thermostatInterface = thermostatArtifact\n ? new ethers.Interface(thermostatArtifact.abi)\n : null;\n const validatorsPerJobCount = Math.max(3, validatorCount);\n const minValidatorsBound = 3;\n const maxValidatorsBound = Math.max(\n minValidatorsBound,\n validatorsPerJobCount\n );\n\n await recordForwardGovernanceCall(\n 'ValidationModule',\n addresses.ValidationModule,\n validationInterface,\n 'setValidatorPool',\n [validators.map((v) => v.address)],\n { notes: 'Populate validator committee pool for demo mission' }\n );\n await recordForwardGovernanceCall(\n 'ValidationModule',\n addresses.ValidationModule,\n validationInterface,\n 'setValidatorBounds',\n [minValidatorsBound, maxValidatorsBound],\n {\n notes: `Require at least ${minValidatorsBound} validators from a pool cap of ${maxValidatorsBound}`,\n before: { quorum, pool: validatorCount },\n after: {\n min: minValidatorsBound.toString(),\n max: maxValidatorsBound.toString(),\n },\n }\n );\n await recordForwardGovernanceCall(\n 'ValidationModule',\n addresses.ValidationModule,\n validationInterface,\n 'setValidatorsPerJob',\n [validatorsPerJobCount],\n {\n notes: `Assign ${validatorsPerJobCount} validators to each flagship job`,\n before: { requested: validatorCount.toString() },\n }\n );\n await recordForwardGovernanceCall(\n 'ValidationModule',\n addresses.ValidationModule,\n validationInterface,\n 'setRequiredValidatorApprovals',\n [quorum],\n { notes: 'Set quorum for validation success' }\n );\n\n const previousCommitWindow = await validationModule.commitWindow();\n const newCommitWindow = 30n;\n await recordForwardGovernanceCall(\n 'ValidationModule',\n addresses.ValidationModule,\n validationInterface,\n 'setCommitWindow',\n [newCommitWindow],\n {\n notes: 'Tighten commit window to 30 seconds for rapid demo cadence',\n before: { commitWindow: previousCommitWindow.toString() },\n after: { commitWindow: newCommitWindow.toString() },\n }\n );\n const previousRevealWindow = await validationModule.revealWindow();\n const newRevealWindow = 300n;\n await recordForwardGovernanceCall(\n 'ValidationModule',\n addresses.ValidationModule,\n validationInterface,\n 'setRevealWindow',\n [newRevealWindow],\n {\n notes: 'Match reveal horizon to five minutes',\n before: { revealWindow: previousRevealWindow.toString() },\n after: { revealWindow: newRevealWindow.toString() },\n }\n );\n\n const stakeEntries: Array<{\n role: string;\n address: string;\n amount: string;\n txHash: string;\n }> = [];\n\n const workerStakeTx = await stakeManager\n .connect(worker)\n .acknowledgeAndDeposit(agentRole, maxWorkerStake);\n const workerStakeReceipt = await workerStakeTx.wait();\n stakeEntries.push({\n role: 'agent',\n address: worker.address,\n amount: formatUnits(maxWorkerStake, decimals),\n txHash: workerStakeReceipt?.hash || workerStakeTx.hash,\n });\n\n for (const validator of validators) {\n const stakeTx = await stakeManager\n .connect(validator)\n .acknowledgeAndDeposit(validatorRole, maxValidatorStake);\n const receipt = await stakeTx.wait();\n stakeEntries.push({\n role: 'validator',\n address: validator.address,\n amount: formatUnits(maxValidatorStake, decimals),\n txHash: receipt?.hash || stakeTx.hash,\n });\n }\n\n writeReceipt(networkName, 'stake.json', { entries: stakeEntries });\n\n const trackAddresses = [\n employer.address,\n worker.address,\n ...validators.map((v) => v.address),\n ];\n\n const missionRecords: Array<{\n name: string;\n slug: string;\n jobId: string;\n reward: string;\n deadline: string;\n resultURI: string;\n txHash: string;\n receipts: Record<string, string>;\n metadata?: Record<string, unknown>;\n notes?: string;\n }> = [];\n\n const legacySingleJob = resolvedJobs.length === 1;\n\n for (const job of resolvedJobs) {\n console.log(`Starting job: ${job.name}`);\n const { k, n } = job.spec.validation;\n await recordForwardGovernanceCall(\n 'ValidationModule',\n addresses.ValidationModule,\n validationInterface,\n 'setValidatorsPerJob',\n [n],\n { notes: `Select ${n} validators for ${job.name}` }\n );\n await recordForwardGovernanceCall(\n 'ValidationModule',\n addresses.ValidationModule,\n validationInterface,\n 'setRequiredValidatorApprovals',\n [k],\n { notes: `Require ${k} approvals for ${job.name}` }\n );\n const jobDir = path.join('jobs', job.slug);\n const specHash = ethers.keccak256(\n ethers.toUtf8Bytes(JSON.stringify(job.spec))\n );\n const specUri = job.spec.acceptanceCriteriaURI || 'ipfs://aurora-demo-spec';\n const deadline = BigInt(Math.floor(Date.now() / 1000) + job.deadlineOffset);\n\n const postTx = await jobRegistry\n .connect(employer)\n .acknowledgeAndCreateJob(\n job.rewardAmount,\n Number(deadline),\n specHash,\n specUri\n );\n const postReceipt = await postTx.wait();\n let jobId = 0n;\n if (postReceipt && postReceipt.logs) {\n for (const log of postReceipt.logs) {\n try {\n const parsed = jobRegistry.interface.parseLog(log);\n if (parsed.name === 'JobCreated') {\n jobId = parsed.args.jobId as bigint;\n break;\n }\n } catch {\n continue;\n }\n }\n }\n if (jobId === 0n) {\n throw new Error('Job creation receipt is missing its JobCreated event');\n }\n const postRecord = {\n jobId: jobId.toString(),\n txHash: postReceipt?.hash || postTx.hash,\n reward: formatUnits(job.rewardAmount, decimals),\n deadline: deadline.toString(),\n specHash,\n specPath: job.specPath,\n metadata: job.metadata,\n };\n writeReceipt(networkName, path.join(jobDir, 'post.json'), postRecord);\n if (legacySingleJob) {\n writeReceipt(networkName, 'postJob.json', postRecord);\n }\n\n const applyTx = await jobRegistry\n .connect(worker)\n .acknowledgeAndApply(jobId, job.agentSubdomain, []);\n await applyTx.wait();\n\n const resultHash = ethers.keccak256(ethers.toUtf8Bytes(job.resultUri));\n const submitTx = await jobRegistry\n .connect(worker)\n .submit(jobId, resultHash, job.resultUri, job.agentSubdomain, []);\n const submitReceipt = await submitTx.wait();\n\n // Submission seeds a future block; selection must finish before commits.\n console.log(`Job ${jobId}: selecting the validator committee`);\n const selectionTarget = await validationModule.selectionBlock(jobId);\n const selectionTimeout = Date.now() + 120_000;\n while (BigInt(await provider.getBlockNumber()) <= selectionTarget) {\n if (Date.now() > selectionTimeout) {\n throw new Error(\n `Timed out waiting for validator selection for job ${jobId}`\n );\n }\n if (chain.chainId === 31337n) {\n await provider.send('evm_mine', []);\n } else {\n await new Promise((resolve) => setTimeout(resolve, 1_000));\n }\n }\n await (\n await validationModule.selectValidators(\n jobId,\n ethers.toBigInt(randomBytes(32))\n )\n ).wait();\n const selectedAddresses: string[] = await validationModule.validators(\n jobId\n );\n if (\n selectedAddresses.length !== n ||\n new Set(selectedAddresses.map((address) => address.toLowerCase()))\n .size !== n\n )\n throw new Error(\n `Job ${jobId}: expected ${n} distinct selected validators`\n );\n const jobValidators = selectedAddresses.map((address) => {\n const signer = validators.find(\n (validator) => validator.address.toLowerCase() === address.toLowerCase()\n );\n if (!signer)\n throw new Error(\n `Job ${jobId}: selected validator has no configured local signer`\n );\n return signer;\n });\n\n const submitRecord = {\n worker: worker.address,\n txHash: submitReceipt?.hash || submitTx.hash,\n resultURI: job.resultUri,\n resultHash,\n metadata: job.metadata,\n };\n writeReceipt(networkName, path.join(jobDir, 'submit.json'), submitRecord);\n if (legacySingleJob) {\n writeReceipt(networkName, 'submit.json', submitRecord);\n }\n\n const nonce = (await validationModule.jobNonce(jobId)).valueOf() as bigint;\n const specHashOnChain = await jobRegistry.getSpecHash(jobId);\n const domainSeparator = await validationModule.DOMAIN_SEPARATOR();\n const validatorSubdomain =\n process.env.AURORA_VALIDATOR_SUBDOMAIN || 'aurora-validator';\n const commitRecords: Array<{\n address: string;\n commitTx: string;\n revealTx: string;\n commitHash: string;\n salt: string;\n }> = [];\n\n for (const validator of jobValidators) {\n const plan = deriveCommitPlan(\n jobId,\n true,\n validator.address,\n nonce,\n specHashOnChain,\n chain.chainId,\n domainSeparator\n );\n const commitTx = await validationModule\n .connect(validator)\n .commitValidation(jobId, plan.commitHash, validatorSubdomain, []);\n const commitReceipt = await commitTx.wait();\n commitRecords.push({\n address: validator.address,\n commitTx: commitReceipt?.hash || commitTx.hash,\n revealTx: '',\n commitHash: plan.commitHash,\n salt: plan.salt,\n });\n }\n\n console.log(\n `Job ${jobId}: ${commitRecords.length} votes committed; entering reveal`\n );\n const commitWindowSeconds = Number(await validationModule.commitWindow());\n await advanceTime(provider, commitWindowSeconds + 1);\n\n for (let i = 0; i < jobValidators.length; i++) {\n const validator = jobValidators[i];\n const revealTx = await validationModule\n .connect(validator)\n .revealValidation(\n jobId,\n true,\n ethers.ZeroHash,\n commitRecords[i].salt,\n validatorSubdomain,\n []\n );\n const revealReceipt = await revealTx.wait();\n commitRecords[i].revealTx = revealReceipt?.hash || revealTx.hash;\n }\n\n const balancesBefore = new Map<string, bigint>();\n for (const addr of trackAddresses) {\n balancesBefore.set(addr, await token.balanceOf(addr));\n }\n\n const finalizeTx = await validationModule\n .connect(jobValidators[0])\n .finalize(jobId);\n const finalizeReceipt = await finalizeTx.wait();\n console.log(`Job ${jobId}: validation complete; settling employer escrow`);\n const settlementTx = await jobRegistry.connect(employer).finalize(jobId);\n const settlementReceipt = await settlementTx.wait();\n const settledJob = await jobRegistry.jobs(jobId);\n const settledMetadata = await jobRegistry.decodeJobMetadata(\n settledJob.packedMetadata\n );\n if (settledMetadata.state !== 6n || !settledMetadata.success) {\n throw new Error(\n `Job ${jobId} did not reach successful finalized settlement`\n );\n }\n\n const payouts: Record<\n string,\n { before: string; after: string; delta: string }\n > = {};\n for (const addr of trackAddresses) {\n const before = balancesBefore.get(addr) || 0n;\n const after = await token.balanceOf(addr);\n payouts[addr] = {\n before: formatUnits(before, decimals),\n after: formatUnits(after, decimals),\n delta: formatUnits(after - before, decimals),\n };\n }\n\n const validateRecord = {\n jobId: jobId.toString(),\n committeeSize: n,\n requiredApprovals: k,\n approvalThreshold: Number(await validationModule.approvalThreshold()),\n validators: commitRecords,\n finalizeTx: finalizeReceipt?.hash || finalizeTx.hash,\n commits: commitRecords.length,\n reveals: commitRecords.length,\n };\n writeReceipt(\n networkName,\n path.join(jobDir, 'validate.json'),\n validateRecord\n );\n if (legacySingleJob) {\n writeReceipt(networkName, 'validate.json', validateRecord);\n }\n\n const finalizeRecord = {\n jobId: jobId.toString(),\n chainId: chain.chainId.toString(),\n status: 'Finalized',\n success: settledMetadata.success,\n txHash: settlementReceipt?.hash || settlementTx.hash,\n blockNumber: settlementReceipt?.blockNumber,\n payouts,\n };\n writeReceipt(\n networkName,\n path.join(jobDir, 'finalize.json'),\n finalizeRecord\n );\n if (legacySingleJob) {\n writeReceipt(networkName, 'finalize.json', finalizeRecord);\n }\n\n missionRecords.push({\n name: job.name,\n slug: job.slug,\n jobId: jobId.toString(),\n reward: formatUnits(job.rewardAmount, decimals),\n deadline: deadline.toString(),\n resultURI: job.resultUri,\n txHash: postReceipt?.hash || postTx.hash,\n receipts: {\n post: path.join(jobDir, 'post.json'),\n submit: path.join(jobDir, 'submit.json'),\n validate: path.join(jobDir, 'validate.json'),\n finalize: path.join(jobDir, 'finalize.json'),\n },\n metadata: job.metadata,\n notes: job.notes,\n });\n\n console.log(\n `🛰️ Finalized mission job ${job.slug} (jobId ${jobId.toString()})`\n );\n }\n\n writeReceipt(networkName, 'mission.json', {\n scope: REPORT_SCOPE,\n version: missionConfig?.version || '1.0',\n description: missionConfig?.description,\n jobs: missionRecords,\n });\n\n let thermostatUpdates: ThermostatUpdate[] = [];\n if (thermostatConfig && thermostat && thermostatInterface) {\n thermostatUpdates = await applyThermostatConfig(\n thermostat,\n thermostatInterface,\n recordForwardGovernanceCall,\n thermostatConfig\n );\n }\n\n await recordForwardGovernanceCall(\n 'StakeManager',\n addresses.StakeManager,\n stakeManager.interface,\n 'setRoleMinimums',\n [\n stakeMinimumBaseline.agent,\n stakeMinimumBaseline.validator,\n stakeMinimumBaseline.platform,\n ],\n {\n notes: 'Restore production minimum stake thresholds',\n before: {\n agent: formatUnits(stakeMinimumAdjusted.agent, decimals),\n validator: formatUnits(stakeMinimumAdjusted.validator, decimals),\n platform: formatUnits(stakeMinimumAdjusted.platform, decimals),\n },\n after: {\n agent: formatUnits(stakeMinimumBaseline.agent, decimals),\n validator: formatUnits(stakeMinimumBaseline.validator, decimals),\n platform: formatUnits(stakeMinimumBaseline.platform, decimals),\n },\n }\n );\n\n await recordForwardGovernanceCall(\n 'JobRegistry',\n addresses.JobRegistry,\n jobRegistry.interface,\n 'setJobStake',\n [originalJobStake],\n {\n notes: 'Return job stake policy to its baseline value',\n before: { stake: formatUnits(adjustedJobStake, decimals) },\n after: { stake: formatUnits(originalJobStake, decimals) },\n }\n );\n\n writeReceipt(networkName, 'governance.json', {\n actions: governanceActions,\n thermostat: thermostatUpdates,\n });\n\n console.log(\n `✅ AURORA demo completed. Jobs finalized: ${missionRecords.length}.`\n );\n}\n\nmain().catch((err) => {\n console.error(err);\n process.exit(1);\n});\n","format":"text","sha256":"79e04444201d7c7e566958340442de8d59c7b479c77755d62217b46da89d3b55","bytes":51566,"download":"/AGIJobsv0/examples/79e04444201d7c7e-aurora.demo.ts","source":"https://github.com/MontrealAI/AGIJobsv0/blob/5b4cebb309a83a7a6749d8911d8bf96a1921e042/demo/aurora/aurora.demo.ts"},{"file":"demo/aurora/bin/aurora-report.ts","content":"#!/usr/bin/env ts-node\nimport fs from 'fs';\nimport path from 'path';\n\ninterface GovernanceLog {\n actions?: Array<Record<string, any>>;\n thermostat?: Array<Record<string, string>>;\n}\n\nconst net =\n process.env.NETWORK ||\n (process.env.CHAIN_ID === '31337' ? 'localhost' : 'localhost');\nconst scope = process.env.AURORA_REPORT_SCOPE || 'aurora';\nconst reportTitle =\n process.env.AURORA_REPORT_TITLE ||\n (scope === 'aurora'\n ? 'Project AURORA — Mission Report'\n : `Mission Report — ${scope\n .replace(/[-_]/g, ' ')\n .replace(/\\b\\w/g, (c) => c.toUpperCase())}`);\n\nconst outDir = path.join('reports', net, scope, 'receipts');\nconst reportFileName =\n scope === 'aurora' ? 'aurora-report.md' : `${scope}-report.md`;\nconst mdFile = path.join('reports', net, scope, reportFileName);\n\nfunction load(relativePath: string) {\n const p = path.join(outDir, relativePath);\n return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : null;\n}\n\nfunction section(title: string): string {\n return '\\n## ' + title + '\\n';\n}\n\nfunction renderKeyValues(record: Record<string, string>): string {\n return Object.entries(record)\n .map(([key, value]) => `${key}: ${value}`)\n .join(', ');\n}\n\n(async () => {\n fs.mkdirSync(path.dirname(mdFile), { recursive: true });\n\n const parts: string[] = [];\n parts.push(`# ${reportTitle}`);\n parts.push('');\n\n const mission = load('mission.json');\n const deploy = load('deploy.json');\n const stake = load('stake.json');\n const governance = load('governance.json') as GovernanceLog | null;\n const placeholder =\n mission?.evidenceClass === 'placeholder' ||\n deploy?.evidenceClass === 'placeholder';\n if (placeholder) {\n parts.push(\n '**PLACEHOLDER FIXTURE — no on-chain work or settlement was executed.**'\n );\n }\n\n if (mission) {\n parts.push(section('Mission Summary'));\n if (mission.description) {\n parts.push(String(mission.description));\n }\n const summaryLines: string[] = [];\n if (mission.scope) summaryLines.push('Scope: ' + mission.scope);\n if (mission.version) summaryLines.push('Version: ' + mission.version);\n if (Array.isArray(mission.jobs) && mission.jobs.length > 0) {\n summaryLines.push(\n (placeholder ? 'Configured fixture jobs: ' : 'Jobs executed: ') +\n mission.jobs.length\n );\n }\n if (summaryLines.length > 0) {\n parts.push('- ' + summaryLines.join('\\n- '));\n }\n }\n\n if (deploy?.contracts && Object.keys(deploy.contracts).length > 0) {\n parts.push(section('Deployment Summary'));\n parts.push('| Module | Address |');\n parts.push('| --- | --- |');\n for (const [name, address] of Object.entries(\n deploy.contracts as Record<string, string>\n )) {\n parts.push('| ' + name + ' | `' + address + '` |');\n }\n }\n\n const missionJobs: Array<Record<string, any>> =\n mission && Array.isArray(mission.jobs) ? mission.jobs : [];\n\n if (missionJobs.length === 0) {\n const post = load('postJob.json');\n const submit = load('submit.json');\n const validate = load('validate.json');\n const finalize = load('finalize.json');\n\n if (post) {\n parts.push(section('Job Creation'));\n parts.push('- **Job ID**: ' + (post.jobId || 'n/a'));\n if (post.txHash) parts.push('- **Transaction**: `' + post.txHash + '`');\n if (post.reward) parts.push('- **Reward**: ' + post.reward);\n if (post.deadline) parts.push('- **Deadline**: ' + post.deadline);\n if (post.specHash) parts.push('- **Spec hash**: `' + post.specHash + '`');\n if (post.specUri) parts.push('- **Spec URI**: ' + post.specUri);\n }\n\n if (submit) {\n parts.push(section('Job Submission'));\n if (submit.worker) parts.push('- **Worker**: `' + submit.worker + '`');\n if (submit.txHash)\n parts.push('- **Submission tx**: `' + submit.txHash + '`');\n if (submit.resultURI) parts.push('- **Result URI**: ' + submit.resultURI);\n }\n\n if (validate) {\n parts.push(section('Validation Phase'));\n if (validate.validators && Array.isArray(validate.validators)) {\n parts.push('- **Validators**:');\n for (const validator of validate.validators as Array<\n Record<string, unknown>\n >) {\n parts.push(\n ` - ${validator.address}: commit \\`${validator.commitTx}\\`, reveal \\`${validator.revealTx}\\``\n );\n }\n }\n if (validate.finalizeTx) {\n parts.push('- Finalize tx: `' + validate.finalizeTx + '`');\n }\n }\n\n if (finalize?.payouts && Object.keys(finalize.payouts).length > 0) {\n parts.push(section('Payouts'));\n for (const [address, payout] of Object.entries(\n finalize.payouts as Record<string, any>\n )) {\n parts.push(\n `- ${address}: ${payout.before} → ${payout.after} (Δ ${payout.delta})`\n );\n }\n }\n } else {\n for (const job of missionJobs) {\n const jobName = typeof job.name === 'string' ? job.name : 'Mission Job';\n const jobSlug =\n typeof job.slug === 'string'\n ? job.slug\n : jobName.toLowerCase().replace(/[^a-z0-9]+/g, '-');\n\n parts.push(section(`Job — ${jobName}`));\n if (job.notes) {\n parts.push('*' + String(job.notes) + '*');\n }\n\n const post = job.receipts?.post\n ? load(job.receipts.post)\n : load(path.join('jobs', jobSlug, 'post.json'));\n if (post) {\n parts.push('- **Job ID**: ' + (post.jobId || 'n/a'));\n if (post.txHash) parts.push('- **Transaction**: `' + post.txHash + '`');\n if (post.reward) parts.push('- **Reward**: ' + post.reward);\n if (post.deadline) parts.push('- **Deadline**: ' + post.deadline);\n if (post.specHash)\n parts.push('- **Spec hash**: `' + post.specHash + '`');\n if (post.specUri) parts.push('- **Spec URI**: ' + post.specUri);\n }\n\n const submit = job.receipts?.submit\n ? load(job.receipts.submit)\n : load(path.join('jobs', jobSlug, 'submit.json'));\n if (submit) {\n if (submit.worker) parts.push('- **Worker**: `' + submit.worker + '`');\n if (submit.txHash)\n parts.push('- **Submission tx**: `' + submit.txHash + '`');\n if (submit.resultURI)\n parts.push('- **Result URI**: ' + submit.resultURI);\n }\n\n const validate = job.receipts?.validate\n ? load(job.receipts.validate)\n : load(path.join('jobs', jobSlug, 'validate.json'));\n if (validate?.validators && Array.isArray(validate.validators)) {\n parts.push('- **Validators**:');\n for (const validator of validate.validators as Array<\n Record<string, unknown>\n >) {\n parts.push(\n ` - ${validator.address}: commit \\`${validator.commitTx}\\`, reveal \\`${validator.revealTx}\\``\n );\n }\n if (validate.finalizeTx) {\n parts.push('- Finalize tx: `' + validate.finalizeTx + '`');\n }\n }\n\n const finalize = job.receipts?.finalize\n ? load(job.receipts.finalize)\n : load(path.join('jobs', jobSlug, 'finalize.json'));\n if (finalize?.payouts && Object.keys(finalize.payouts).length > 0) {\n parts.push('- **Payouts**:');\n for (const [address, payout] of Object.entries(\n finalize.payouts as Record<string, any>\n )) {\n parts.push(\n ` - ${address}: ${payout.before} → ${payout.after} (Δ ${payout.delta})`\n );\n }\n }\n }\n }\n\n if (stake?.entries && Array.isArray(stake.entries)) {\n parts.push(section('Stake Operations'));\n for (const entry of stake.entries as Array<Record<string, unknown>>) {\n parts.push(\n '- ' +\n entry.role +\n ' `' +\n entry.address +\n '` staked ' +\n entry.amount +\n ' (tx: `' +\n entry.txHash +\n '`)'\n );\n }\n }\n\n if (governance?.thermostat && governance.thermostat.length > 0) {\n parts.push(section('Thermostat Tuning'));\n for (const update of governance.thermostat) {\n const tx = update.txHash ? ` (tx: \\`${update.txHash}\\`)` : '';\n parts.push(`- ${update.action}: ${update.before} → ${update.after}${tx}`);\n }\n }\n\n if (governance?.actions && governance.actions.length > 0) {\n parts.push(section('Governance & Controls'));\n for (const action of governance.actions) {\n const header =\n '- **' +\n action.target +\n '.' +\n action.method +\n '** (' +\n action.type +\n ') — tx `' +\n action.txHash +\n '`';\n parts.push(header);\n if (action.notes) {\n parts.push(' - Notes: ' + action.notes);\n }\n if (action.params) {\n parts.push(' - Params: ' + JSON.stringify(action.params));\n }\n if (action.before) {\n parts.push(\n ' - Before: ' +\n renderKeyValues(action.before as Record<string, string>)\n );\n }\n if (action.after) {\n parts.push(\n ' - After: ' +\n renderKeyValues(action.after as Record<string, string>)\n );\n }\n }\n }\n\n fs.writeFileSync(mdFile, parts.join('\\n') + '\\n');\n console.log('Wrote ' + mdFile);\n})();\n","format":"text","sha256":"c67b764cecb9ef53a07199ac748b66114b9a56d98bf28a2b905435e00c34953a","bytes":9202,"download":"/AGIJobsv0/examples/c67b764cecb9ef53-aurora-report.ts","source":"https://github.com/MontrealAI/AGIJobsv0/blob/5b4cebb309a83a7a6749d8911d8bf96a1921e042/demo/aurora/bin/aurora-report.ts"}]}
FROM READING TO A REPRODUCIBLE RUN
Try the selected path. Disposable local chain From the repository root, use nvm install, nvm use and npm ci with the pinned Node/npm versions. The local driver uses mock assets; its owned development node stops at the end. If port 8545 is occupied, use DEMO_PORT=18545.
Complete environment setup ↗ SELECTED EXECUTION PATH Copy
npm run demo:aurora:localWhat you should observe One local job completes; inspect reports/localhost/aurora/ and its transaction evidence. Mock tokens and synthetic work are used.
The source inspector above reads bundled repository material. Local commands run separately on your computer. Recorded examples may contain historical timestamps, placeholders and simulated metrics.
MAKE IT YOUR OWN
One useful experiment. Change an acceptance condition in a copied specification and compare the resulting validation requirements. Do not treat a placeholder URI as evidence.
THE SYSTEM, MADE VISIBLE
Architecture & relationships Architecture diagram · source preserved below
View original Mermaid source graph TD
G[Governance (Safe/Timelock)]
JR[JobRegistry]
SM[StakeManager]
VM[ValidationModule]
DM[DisputeModule]
SP[SystemPause]
RE[ReputationEngine]
TH[Thermostat]
EO[EnergyOracle]
MB[RewardEngineMB]
FP[FeePool]
G --> JR & SM & DM & SP & TH
EO --> MB --> FP
JR --> VM --> DM --> JR
JR --> RE
JR --> SMAll standalone flowcharts (2)
WHEN SOMETHING DOESN’T MATCH
Troubleshooting Port already in use Choose DEMO_PORT=18545 before the command. Do not connect a demo to an unrelated or valuable chain.
No final settlement evidence Read the first failed transaction or validation step. Compare the effective mission, committee thresholds and report scope; a partial report is not a completed run.
TRACE THE CHECKS
Verification & next steps This directory has no standalone test source files. Follow the shared implementation and its behavioral tests before making correctness claims.
For live commissioning, consult the production readiness record .
EVERY VARIANT, PRESERVED
Complete document library REPRODUCE & INSPECT
Registered commands Run commands from the repository root after following this demo's guide. Network and owner actions require their documented setup.
npm run demo:asi-takeoff:reportCopy
npm run demo:aurora:localCopy
npm run demo:aurora:reportCopy
Full command catalog and troubleshooting ↗