/** * AI Hub Coordination Worker * ================================================================ * Shared job board + agent heartbeats for James Grunsky's agents * (Claude / ChatGPT / Grok, etc.). * * Endpoints: * GET / /app — PWA dashboard (HTML) * GET /manifest.webmanifest * GET /icon.svg * GET /health * GET /docs — index of public FIELD / Phase A docs * GET /docs/field-brief * GET /docs/one-pager * GET /docs/phone-mvp-spec * GET /docs/label-protocol * GET /docs/handoff * GET /docs/morada-sizing * GET /docs/adp-fct-labor-wire-schema * GET /docs/adp-sample-2026-09-03 * GET /docs/adp-sample-2026-09-03-by-driver * GET /docs/fct-v220-sms-spec * GET /docs/orchard-observation-contract * GET /docs/mock /mock /mock/ — Phase A picker HUD HTML mock * GET /jobs?status= * POST /jobs (auth) * GET /jobs/:id * PATCH /jobs/:id (auth) * POST /jobs/:id/complete (auth) * POST /jobs/:id/decision (auth) — decision: accepted|changes_requested * GET /jobs/:id/comments — thread for a job (public read) * POST /jobs/:id/comments (auth) — { agent, body } * POST /heartbeat (auth) — { agent, note? } * GET /agents * GET /james — James ↔ Agents conversation UI * POST /james (auth) — { body, to?: all|claude|chatgpt|grok } * POST /reply (auth) — agent reply { from, body, to?: james|all|claude|chatgpt|grok } * GET /james/messages — conversation log (James + agent replies; public read) * GET /inbox/:agent — per-agent inbox (incl. /inbox/james; public read) * GET /inbox/james — James's inbox of agent replies * POST /mcp GET /mcp — read-only MCP JSON-RPC (optional Hub key) * GET /srp/state — SRP research-state snapshot (public read) * GET /srp/governance — SRP standing delegated governance JSON (public read) * GET /docs/hub-mcp-readonly — MCP + /srp docs * GET /docs/hub-research-orchestration — research queue protocol * GET /docs/orchestration-source-manifest — source+SHA256 for review * GET /docs/orchestration.js.txt /orchestration.test.mjs.txt /orchestration-index.js.txt * GET /research/schema|/queue|/queue/highest|/brief|/claims|/decisions|/jobs/:id * POST /research/jobs|/jobs/:id/transition|/jobs/:id/handoff|/brief/generate|/seed|/claims (auth) * OPTIONS (CORS preflight) * * Auth: * Mutating routes require X-AI-Hub-Key matching HUB_KEY secret. * MCP read tools: public read; if X-AI-Hub-Key or Bearer provided, must match HUB_KEY. * Do not log or echo HUB_KEY / X-AI-Hub-Key. * * KV binding: AI_HUB * job: — job JSON * idx:jobs — JSON array of job ids * comments: — JSON array of {id, agent, body, at}; newest last * agent::heartbeat — last heartbeat JSON * james:messages — conversation log (max 100); {id, from, to, body, at, ts} * inbox: — per-recipient inbox (max 50) * research:job: / research:idx / research:transitions: * research:decisions:* / research:claim:* / research:brief:latest * orch:idempotency: — research handoff idempotency * * Secret: HUB_KEY */ import { dashboardHtml, manifestJson, iconSvg } from './dashboard.js'; import fieldBriefMd from './docs/field-brief.md'; import onePagerMd from './docs/one-pager.md'; import phoneMvpSpecMd from './docs/phone-mvp-spec.md'; import labelProtocolMd from './docs/label-protocol.md'; import handoffMd from './docs/handoff.md'; import moradaSizingMd from './docs/morada-sizing.md'; import adpFctLaborWireSchemaMd from './docs/adp-fct-labor-wire-schema.md'; import adpSample20260903Csv from './docs/adp-sample-2026-09-03.csv'; import adpSample20260903ByDriverCsv from './docs/adp-sample-2026-09-03-by-driver.csv'; import fctV220SmsSpecMd from './docs/fct-v220-sms-spec.md'; import orchardObservationContractMd from './docs/orchard-observation-contract.md'; import mockHtml from './docs/mock.html'; import fcgeBinPlacementEvidenceMd from './docs/fcge-bin-placement-evidence.md'; import fcgePlannedAssetRegisterMd from './docs/fcge-planned-asset-register.md'; import fcgeBinMapHtml from './docs/fcge-bin-map.html'; import fcgeBinMapSatelliteHtml from './docs/fcge-bin-map-satellite.html'; import fcgePlannedAssetRegisterJson from './docs/fcge-planned-asset-register.json'; import fcgeBinPlacementV3Json from './docs/fcge-bin-placement-v3.json'; import fcgeBinMapJpg from './docs/assets/fcge-bin-map.jpg'; import jamesPlacementCorrectionsJpg from './docs/assets/james-placement-corrections.jpg'; import binPlacementSatelliteV3Jpg from './docs/assets/bin-placement-satellite-v3.jpg'; import fcgeBinMapV3PlacedJpg from './docs/assets/fcge-bin-map-v3-placed.jpg'; import fcgeBinmapInventoryV2Md from './docs/fcge-binmap-inventory-v2.md'; import binMapInventoryV21Html from './docs/bin-map-inventory-v2.1.html'; import inventoryBook2CurrentV21Json from './docs/inventory-book2-current-v2.1.json'; import fcgeAssetCrosswalkV21Json from './docs/fcge-asset-crosswalk-v2.1.json'; import fcgeBinmapReviewManifestV21Json from './docs/fcge-binmap-review-manifest-v2.1.json'; import whiteboardLayoutV1Json from './docs/whiteboard-layout-v1.json'; import fcgeBinmapWithInventoryV21Png from './docs/assets/fcge-binmap-with-inventory-v2.1.png'; import binMapInventoryV22Html from './docs/bin-map-inventory-v2.2.html'; import inventoryBook2CurrentV22Json from './docs/inventory-book2-current-v2.2.json'; import fcgeAssetCrosswalkV22Json from './docs/fcge-asset-crosswalk-v2.2.json'; import fcgeBinmapReviewManifestV22Json from './docs/fcge-binmap-review-manifest-v2.2.json'; import fcgeBinmapWithInventoryV22Png from './docs/assets/fcge-binmap-with-inventory-v2.2.png'; import wave2bA1A2EvidenceMd from './docs/wave2b-a1-a2-evidence.md'; import wave2bA1A2EvidenceMemoMd from './docs/wave2b-a1-a2-evidence-memo.md'; import wave2bA1A2RebuildPy from './docs/wave2b-a1-a2-rebuild.py.txt'; import wave2bA1A2AnnualCsv from './docs/wave2b-a1-a2-annual.csv'; import wave2bA1A2AnnualJson from './docs/wave2b-a1-a2-annual.json'; import wave2bA1GapMonthlyCsv from './docs/wave2b-a1-gap-monthly.csv'; import wave2bA2GapMonthlyCsv from './docs/wave2b-a2-gap-monthly.csv'; import wave2bA1A2DistributionsJson from './docs/wave2b-a1-a2-distributions.json'; import wave2bA1A2ExampleYearsJson from './docs/wave2b-a1-a2-example-years.json'; import wave2bA1A2ProvenanceJson from './docs/wave2b-a1-a2-provenance.json'; import wave2bA1A2CorrelationJson from './docs/wave2b-a1-a2-correlation.json'; import wave2bA1A2CleanupAndBMd from './docs/wave2b-a1-a2-cleanup-and-b.md'; import srpGovernanceMd from './docs/srp-governance.md'; import wave2bA1A2OverlapCorrelationJson from './docs/wave2b-a1-a2-overlap-correlation.json'; import wave2bMechanicalQuantileYearsJson from './docs/wave2b-mechanical-quantile-years.json'; import wave2bTrackBFredStatusJson from './docs/wave2b-track-b-fred-status.json'; import wave2bTrackBBeaBlsMd from './docs/wave2b-track-b-bea-bls.md'; import wave2bTrackBBeaBlsProvenanceJson from './docs/wave2b-track-b-bea-bls-provenance.json'; import wave2bTrackBSimpleAnnualCsv from './docs/wave2b-track-b-simple-annual.csv'; import wave2bTrackBSimpleMonthlyCsv from './docs/wave2b-track-b-simple-monthly.csv'; import wave2bTrackBSimplePairsJson from './docs/wave2b-track-b-simple-pairs.json'; import wave2bTrackBRebuildPy from './docs/wave2b-track-b-rebuild.py.txt'; import wave2bTrackBFredRetryJson from './docs/wave2b-track-b-fred-retry.json'; import wave2bA229rxYoyMonthlyCsv from './docs/wave2b-a229rx-yoy-monthly.csv'; import wave2bA2BDiscriminationMd from './docs/wave2b-a2-b-discrimination.md'; import wave2bA2BDiscriminationAnnualCsv from './docs/wave2b-a2-b-discrimination-annual.csv'; import wave2bA2BDiscriminationCrosstabCsv from './docs/wave2b-a2-b-discrimination-crosstab.csv'; import wave2bA2BDiscriminationJson from './docs/wave2b-a2-b-discrimination.json'; import wave2bA2BDiscriminationRulesJson from './docs/wave2b-a2-b-discrimination-rules-v1.json'; import wave2bA2BDiscriminationProvenanceJson from './docs/wave2b-a2-b-discrimination-provenance.json'; import wave2bA2BDiscriminationPy from './docs/wave2b-a2-b-discrimination.py.txt'; import wave2bA2DecompositionMd from './docs/wave2b-a2-decomposition.md'; import wave2bA2DecompositionJson from './docs/wave2b-a2-decomposition.json'; import wave2bA2DecompositionAnnualCsv from './docs/wave2b-a2-decomposition-annual.csv'; import wave2bA2DecompositionCorrelationsCsv from './docs/wave2b-a2-decomposition-correlations.csv'; import wave2bA2DecompositionThresholdCsv from './docs/wave2b-a2-decomposition-threshold-sensitivity.csv'; import wave2bA2DecompositionRollingCsv from './docs/wave2b-a2-decomposition-rolling15y.csv'; import wave2bA2DecompositionPy from './docs/wave2b-a2-decomposition.py.txt'; import wave2bM1dCalibratedMissMd from './docs/wave2b-m1d-calibrated-miss.md'; import wave2bM1dSummaryJson from './docs/wave2b-m1d-summary.json'; import wave2bM1dProtocolJson from './docs/wave2b-m1d-protocol.json'; import wave2bM1dMetricsCsv from './docs/wave2b-m1d-metrics-by-scheme.csv'; import wave2bM1dOosExpandingCsv from './docs/wave2b-m1d-oos-predictions-expanding.csv'; import wave2bM1dOosRollingCsv from './docs/wave2b-m1d-oos-predictions-rolling.csv'; import wave2bM1dPy from './docs/wave2b-m1d-calibrated-miss.py.txt'; import wave2bM1dFinalBenchmarkMd from './docs/wave2b-m1d-final-benchmark.md'; import wave2bM1dFinalProtocolJson from './docs/wave2b-m1d-final-protocol.json'; import wave2bM1dFinalSummaryJson from './docs/wave2b-m1d-final-summary.json'; import wave2bM1dFinalMetricsCsv from './docs/wave2b-m1d-final-metrics.csv'; import wave2bM1dFinalAnnualExpandingCsv from './docs/wave2b-m1d-final-annual-errors-expanding.csv'; import wave2bM1dFinalAnnualRollingCsv from './docs/wave2b-m1d-final-annual-errors-rolling.csv'; import wave2bM1dFinalOosExpandingCsv from './docs/wave2b-m1d-final-oos-expanding.csv'; import wave2bM1dFinalOosRollingCsv from './docs/wave2b-m1d-final-oos-rolling.csv'; import wave2bM1dFinalPy from './docs/wave2b-m1d-final-benchmark.py.txt'; import m1DecisionsMd from './docs/m1-decisions.md'; import wave2bM1LongFeasibilityMd from './docs/wave2b-m1-long-feasibility.md'; import wave2bA1A2TimeseriesPng from './docs/assets/wave2b-a1-a2-timeseries.png'; import srpStandingDelegatedGovernanceMd from './docs/srp-standing-delegated-governance.md'; import { jamesHtml } from './james.js'; import hubMcpReadonlyMd from './docs/hub-mcp-readonly.md'; import { handleMcp, buildSrpState, buildSrpGovernance, buildSrpEvidence } from './mcp.js'; import { handleResearchRoutes, isMaterialForJames, parseStructuredHandoff } from './orchestration.js'; import hubResearchOrchestrationMd from './docs/hub-research-orchestration.md'; import orchestrationSourceManifestMd from './docs/orchestration-source-manifest.md'; import orchestrationJsTxt from './docs/orchestration.js.txt'; import orchestrationTestTxt from './docs/orchestration.test.mjs.txt'; import orchestrationIndexTxt from './docs/orchestration-index.js.txt'; import orchestrationWranglerTxt from './docs/orchestration-wrangler.toml.txt'; import orchestrationPackageTxt from './docs/orchestration-package.json.txt'; import orchestrationManifestJson from './docs/orchestration-manifest.json'; const PUBLIC_DOCS = { 'field-brief': { body: fieldBriefMd, type: 'text/markdown; charset=utf-8' }, 'one-pager': { body: onePagerMd, type: 'text/markdown; charset=utf-8' }, 'phone-mvp-spec': { body: phoneMvpSpecMd, type: 'text/markdown; charset=utf-8' }, 'label-protocol': { body: labelProtocolMd, type: 'text/markdown; charset=utf-8' }, handoff: { body: handoffMd, type: 'text/markdown; charset=utf-8' }, 'morada-sizing': { body: moradaSizingMd, type: 'text/markdown; charset=utf-8' }, 'adp-fct-labor-wire-schema': { body: adpFctLaborWireSchemaMd, type: 'text/markdown; charset=utf-8' }, 'adp-sample-2026-09-03': { body: adpSample20260903Csv, type: 'text/csv; charset=utf-8' }, 'adp-sample-2026-09-03-by-driver': { body: adpSample20260903ByDriverCsv, type: 'text/csv; charset=utf-8' }, 'fct-v220-sms-spec': { body: fctV220SmsSpecMd, type: 'text/markdown; charset=utf-8' }, 'orchard-observation-contract': { body: orchardObservationContractMd, type: 'text/markdown; charset=utf-8' }, mock: { body: mockHtml, type: 'text/html; charset=utf-8' }, 'fcge-bin-placement-evidence': { body: fcgeBinPlacementEvidenceMd, type: 'text/markdown; charset=utf-8' }, 'fcge-planned-asset-register': { body: fcgePlannedAssetRegisterMd, type: 'text/markdown; charset=utf-8' }, 'fcge-bin-map.html': { body: fcgeBinMapHtml, type: 'text/html; charset=utf-8' }, 'fcge-bin-map-satellite.html': { body: fcgeBinMapSatelliteHtml, type: 'text/html; charset=utf-8' }, 'fcge-planned-asset-register.json': { body: fcgePlannedAssetRegisterJson, type: 'application/json; charset=utf-8' }, 'fcge-bin-placement-v3.json': { body: fcgeBinPlacementV3Json, type: 'application/json; charset=utf-8' }, 'fcge-binmap-inventory-v2': { body: fcgeBinmapInventoryV2Md, type: 'text/markdown; charset=utf-8' }, 'bin-map-inventory-v2.1.html': { body: binMapInventoryV21Html, type: 'text/html; charset=utf-8' }, 'inventory-book2-current-v2.1.json': { body: inventoryBook2CurrentV21Json, type: 'application/json; charset=utf-8' }, 'fcge-asset-crosswalk-v2.1.json': { body: fcgeAssetCrosswalkV21Json, type: 'application/json; charset=utf-8' }, 'fcge-binmap-review-manifest-v2.1.json': { body: fcgeBinmapReviewManifestV21Json, type: 'application/json; charset=utf-8' }, 'bin-map-inventory-v2.2.html': { body: binMapInventoryV22Html, type: 'text/html; charset=utf-8' }, 'inventory-book2-current-v2.2.json': { body: inventoryBook2CurrentV22Json, type: 'application/json; charset=utf-8' }, 'fcge-asset-crosswalk-v2.2.json': { body: fcgeAssetCrosswalkV22Json, type: 'application/json; charset=utf-8' }, 'fcge-binmap-review-manifest-v2.2.json': { body: fcgeBinmapReviewManifestV22Json, type: 'application/json; charset=utf-8' }, 'whiteboard-layout-v1.json': { body: whiteboardLayoutV1Json, type: 'application/json; charset=utf-8' }, 'wave2b-a1-a2-evidence': { body: wave2bA1A2EvidenceMd, type: 'text/markdown; charset=utf-8' }, 'wave2b-a1-a2-evidence-memo': { body: wave2bA1A2EvidenceMemoMd, type: 'text/markdown; charset=utf-8' }, 'wave2b-a1-a2-rebuild.py.txt': { body: wave2bA1A2RebuildPy, type: 'text/plain; charset=utf-8' }, 'wave2b-a1-a2-annual.csv': { body: wave2bA1A2AnnualCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-a1-a2-annual.json': { body: wave2bA1A2AnnualJson, type: 'application/json; charset=utf-8' }, 'wave2b-a1-gap-monthly.csv': { body: wave2bA1GapMonthlyCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-a2-gap-monthly.csv': { body: wave2bA2GapMonthlyCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-a1-a2-distributions.json': { body: wave2bA1A2DistributionsJson, type: 'application/json; charset=utf-8' }, 'wave2b-a1-a2-example-years.json': { body: wave2bA1A2ExampleYearsJson, type: 'application/json; charset=utf-8' }, 'wave2b-a1-a2-provenance.json': { body: wave2bA1A2ProvenanceJson, type: 'application/json; charset=utf-8' }, 'wave2b-a1-a2-correlation.json': { body: wave2bA1A2CorrelationJson, type: 'application/json; charset=utf-8' }, 'wave2b-a1-a2-cleanup-and-b': { body: wave2bA1A2CleanupAndBMd, type: 'text/markdown; charset=utf-8' }, 'srp-governance': { body: srpGovernanceMd, type: 'text/markdown; charset=utf-8' }, 'wave2b-a1-a2-overlap-correlation.json': { body: wave2bA1A2OverlapCorrelationJson, type: 'application/json; charset=utf-8' }, 'wave2b-mechanical-quantile-years.json': { body: wave2bMechanicalQuantileYearsJson, type: 'application/json; charset=utf-8' }, 'wave2b-track-b-fred-status.json': { body: wave2bTrackBFredStatusJson, type: 'application/json; charset=utf-8' }, 'wave2b-track-b-bea-bls': { body: wave2bTrackBBeaBlsMd, type: 'text/markdown; charset=utf-8' }, 'wave2b-track-b-bea-bls-provenance.json': { body: wave2bTrackBBeaBlsProvenanceJson, type: 'application/json; charset=utf-8' }, 'wave2b-track-b-simple-annual.csv': { body: wave2bTrackBSimpleAnnualCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-track-b-simple-monthly.csv': { body: wave2bTrackBSimpleMonthlyCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-track-b-simple-pairs.json': { body: wave2bTrackBSimplePairsJson, type: 'application/json; charset=utf-8' }, 'wave2b-track-b-rebuild.py.txt': { body: wave2bTrackBRebuildPy, type: 'text/plain; charset=utf-8' }, 'wave2b-track-b-fred-retry.json': { body: wave2bTrackBFredRetryJson, type: 'application/json; charset=utf-8' }, 'wave2b-a229rx-yoy-monthly.csv': { body: wave2bA229rxYoyMonthlyCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-a2-b-discrimination': { body: wave2bA2BDiscriminationMd, type: 'text/markdown; charset=utf-8' }, 'wave2b-a2-b-discrimination-annual.csv': { body: wave2bA2BDiscriminationAnnualCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-a2-b-discrimination-crosstab.csv': { body: wave2bA2BDiscriminationCrosstabCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-a2-b-discrimination.json': { body: wave2bA2BDiscriminationJson, type: 'application/json; charset=utf-8' }, 'wave2b-a2-b-discrimination-rules-v1.json': { body: wave2bA2BDiscriminationRulesJson, type: 'application/json; charset=utf-8' }, 'wave2b-a2-b-discrimination-provenance.json': { body: wave2bA2BDiscriminationProvenanceJson, type: 'application/json; charset=utf-8' }, 'wave2b-a2-b-discrimination.py.txt': { body: wave2bA2BDiscriminationPy, type: 'text/plain; charset=utf-8' }, 'wave2b-a2-decomposition': { body: wave2bA2DecompositionMd, type: 'text/markdown; charset=utf-8' }, 'wave2b-a2-decomposition.json': { body: wave2bA2DecompositionJson, type: 'application/json; charset=utf-8' }, 'wave2b-a2-decomposition-annual.csv': { body: wave2bA2DecompositionAnnualCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-a2-decomposition-correlations.csv': { body: wave2bA2DecompositionCorrelationsCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-a2-decomposition-threshold-sensitivity.csv': { body: wave2bA2DecompositionThresholdCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-a2-decomposition-rolling15y.csv': { body: wave2bA2DecompositionRollingCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-a2-decomposition.py.txt': { body: wave2bA2DecompositionPy, type: 'text/plain; charset=utf-8' }, 'wave2b-m1d-calibrated-miss': { body: wave2bM1dCalibratedMissMd, type: 'text/markdown; charset=utf-8' }, 'wave2b-m1d-summary.json': { body: wave2bM1dSummaryJson, type: 'application/json; charset=utf-8' }, 'wave2b-m1d-protocol.json': { body: wave2bM1dProtocolJson, type: 'application/json; charset=utf-8' }, 'wave2b-m1d-metrics-by-scheme.csv': { body: wave2bM1dMetricsCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-m1d-oos-predictions-expanding.csv': { body: wave2bM1dOosExpandingCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-m1d-oos-predictions-rolling.csv': { body: wave2bM1dOosRollingCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-m1d-calibrated-miss.py.txt': { body: wave2bM1dPy, type: 'text/plain; charset=utf-8' }, 'wave2b-m1d-final-benchmark': { body: wave2bM1dFinalBenchmarkMd, type: 'text/markdown; charset=utf-8' }, 'wave2b-m1d-final-protocol.json': { body: wave2bM1dFinalProtocolJson, type: 'application/json; charset=utf-8' }, 'wave2b-m1d-final-summary.json': { body: wave2bM1dFinalSummaryJson, type: 'application/json; charset=utf-8' }, 'wave2b-m1d-final-metrics.csv': { body: wave2bM1dFinalMetricsCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-m1d-final-annual-errors-expanding.csv': { body: wave2bM1dFinalAnnualExpandingCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-m1d-final-annual-errors-rolling.csv': { body: wave2bM1dFinalAnnualRollingCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-m1d-final-oos-expanding.csv': { body: wave2bM1dFinalOosExpandingCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-m1d-final-oos-rolling.csv': { body: wave2bM1dFinalOosRollingCsv, type: 'text/csv; charset=utf-8' }, 'wave2b-m1d-final-benchmark.py.txt': { body: wave2bM1dFinalPy, type: 'text/plain; charset=utf-8' }, 'm1-decisions': { body: m1DecisionsMd, type: 'text/markdown; charset=utf-8' }, 'wave2b-m1-long-feasibility': { body: wave2bM1LongFeasibilityMd, type: 'text/markdown; charset=utf-8' }, 'hub-mcp-readonly': { body: hubMcpReadonlyMd, type: 'text/markdown; charset=utf-8' }, 'hub-research-orchestration': { body: hubResearchOrchestrationMd, type: 'text/markdown; charset=utf-8' }, 'orchestration-source-manifest': { body: orchestrationSourceManifestMd, type: 'text/markdown; charset=utf-8' }, 'orchestration.js.txt': { body: orchestrationJsTxt, type: 'text/plain; charset=utf-8' }, 'orchestration.test.mjs.txt': { body: orchestrationTestTxt, type: 'text/plain; charset=utf-8' }, 'orchestration-index.js.txt': { body: orchestrationIndexTxt, type: 'text/plain; charset=utf-8' }, 'orchestration-wrangler.toml.txt': { body: orchestrationWranglerTxt, type: 'text/plain; charset=utf-8' }, 'orchestration-package.json.txt': { body: orchestrationPackageTxt, type: 'application/json; charset=utf-8' }, 'orchestration-manifest.json': { body: orchestrationManifestJson, type: 'application/json; charset=utf-8' }, }; const PUBLIC_ASSETS = { 'fcge-bin-map.jpg': { body: fcgeBinMapJpg, type: 'image/jpeg' }, 'james-placement-corrections.jpg': { body: jamesPlacementCorrectionsJpg, type: 'image/jpeg' }, 'bin-placement-satellite-v3.jpg': { body: binPlacementSatelliteV3Jpg, type: 'image/jpeg' }, 'fcge-bin-map-v3-placed.jpg': { body: fcgeBinMapV3PlacedJpg, type: 'image/jpeg' }, 'fcge-binmap-with-inventory-v2.1.png': { body: fcgeBinmapWithInventoryV21Png, type: 'image/png' }, 'fcge-binmap-with-inventory-v2.2.png': { body: fcgeBinmapWithInventoryV22Png, type: 'image/png' }, 'wave2b-a1-a2-timeseries.png': { body: wave2bA1A2TimeseriesPng, type: 'image/png' }, 'srp-standing-delegated-governance': { body: srpStandingDelegatedGovernanceMd, type: 'text/markdown; charset=utf-8' } }; function docsIndexHtml(origin) { const base = (origin || '').replace(/\/+$/, ''); const links = [ ['Field brief', '/docs/field-brief'], ['One-pager', '/docs/one-pager'], ['Phone MVP spec', '/docs/phone-mvp-spec'], ['Label protocol', '/docs/label-protocol'], ['Handoff (Claude / ChatGPT)', '/docs/handoff'], ['Morada / UNITEC sizing', '/docs/morada-sizing'], ['ADP → FCT labor schema', '/docs/adp-fct-labor-wire-schema'], ['ADP sample 2026-09-03 CSV', '/docs/adp-sample-2026-09-03'], ['FCT v2.2.0 SMS spec', '/docs/fct-v220-sms-spec'], ['Orchard observation contract v0.1.1', '/docs/orchard-observation-contract'], ['FCGE bin-placement evidence (1430)', '/docs/fcge-bin-placement-evidence'], ['FCGE planned asset register v0.2', '/docs/fcge-planned-asset-register'], ['FCGE binmap inventory review v2.2 (1430)', '/docs/fcge-binmap-inventory-v2'], ['WAVE2B A1 vs A2 evidence (SRP b73c)', '/docs/wave2b-a1-a2-evidence'], ['WAVE2B A1/A2 cleanup + Track B draft (SRP b73c)', '/docs/wave2b-a1-a2-cleanup-and-b'], ['WAVE2B mechanical quantile years JSON', '/docs/wave2b-mechanical-quantile-years.json'], ['WAVE2B Track B FRED ingest status JSON', '/docs/wave2b-track-b-fred-status.json'], ['WAVE2B Track B BEA/BLS memo', '/docs/wave2b-track-b-bea-bls'], ['WAVE2B Track B simple monthly pairs CSV', '/docs/wave2b-track-b-simple-monthly.csv'], ['WAVE2B Track B simple annual CSV', '/docs/wave2b-track-b-simple-annual.csv'], ['WAVE2B Track B rebuild script', '/docs/wave2b-track-b-rebuild.py.txt'], ['WAVE2B A2×B discrimination memo (SRP b73c)', '/docs/wave2b-a2-b-discrimination'], ['WAVE2B A2×B discrimination annual CSV', '/docs/wave2b-a2-b-discrimination-annual.csv'], ['WAVE2B A2×B discrimination JSON', '/docs/wave2b-a2-b-discrimination.json'], ['WAVE2B A2 decomposition memo (Astra 4cc1111f)', '/docs/wave2b-a2-decomposition'], ['WAVE2B A2 decomposition JSON', '/docs/wave2b-a2-decomposition.json'], ['WAVE2B A2 decomposition annual CSV', '/docs/wave2b-a2-decomposition-annual.csv'], ['WAVE2B M1-D final bounded benchmark (RETIRE)', '/docs/wave2b-m1d-final-benchmark'], ['WAVE2B M1-D final summary JSON', '/docs/wave2b-m1d-final-summary.json'], ['WAVE2B M1-D final OOS expanding CSV', '/docs/wave2b-m1d-final-oos-expanding.csv'], ['WAVE2B M1-D calibrated miss memo (research)', '/docs/wave2b-m1d-calibrated-miss'], ['WAVE2B M1-D summary JSON', '/docs/wave2b-m1d-summary.json'], ['WAVE2B M1-D OOS expanding CSV', '/docs/wave2b-m1d-oos-predictions-expanding.csv'], ['M1 decisions log (A2 retire + M1D-RETIRE APPROVED)', '/docs/m1-decisions'], ['WAVE2B M1-LONG feasibility (research)', '/docs/wave2b-m1-long-feasibility'], ['SRP delegated governance', '/docs/srp-governance'], ['SRP standing delegated governance', '/docs/srp-standing-delegated-governance'], ['Hub MCP read-only + /srp', '/docs/hub-mcp-readonly'], ['Hub research orchestration (SRP queue)', '/docs/hub-research-orchestration'], ['Orchestration source manifest + SHA256', '/docs/orchestration-source-manifest'], ['Orchestration orchestration.js.txt', '/docs/orchestration.js.txt'], ['Orchestration tests .mjs.txt', '/docs/orchestration.test.mjs.txt'], ['Orchestration index.js.txt', '/docs/orchestration-index.js.txt'], ['Orchestration manifest JSON', '/docs/orchestration-manifest.json'], ['Picker HUD mock', '/mock/'] ] .map( ([label, href]) => `
  • ${label} ${href}
  • ` ) .join('\n'); return ` AI Hub Docs

    AR Cherry — public docs

    Self-contained copies for Claude / ChatGPT (no OneDrive required).

      ${links}

    ← AI Hub dashboard

    `; } function textDocResponse(body, contentType, cors) { return new Response(body, { headers: { 'Content-Type': contentType, 'Cache-Control': 'public, max-age=60', ...cors } }); } const JOB_STATUSES = new Set([ 'open', 'assigned', 'in_progress', 'done', 'accepted', 'changes_requested', 'cancelled' ]); const DECISIONS = new Set(['accepted', 'changes_requested']); const IDX_JOBS = 'idx:jobs'; function corsHeaders() { return { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET, POST, PATCH, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, X-AI-Hub-Key, Authorization', 'Cache-Control': 'no-store' }; } function json(obj, status) { return new Response(JSON.stringify(obj), { status: status || 200, headers: { 'Content-Type': 'application/json', ...corsHeaders() } }); } function hubKeyOk(request, env) { const expected = env.HUB_KEY; if (!expected) return false; const got = request.headers.get('X-AI-Hub-Key'); if (!got) return false; return got === expected; } function requireAuth(request, env) { if (!hubKeyOk(request, env)) { return json({ error: 'unauthorized' }, 401); } return null; } function nowIso() { return new Date().toISOString(); } function newId() { return crypto.randomUUID(); } async function getJobIds(env) { const raw = await env.AI_HUB.get(IDX_JOBS); if (!raw) return []; try { const arr = JSON.parse(raw); return Array.isArray(arr) ? arr : []; } catch (_) { return []; } } async function putJobIds(env, ids) { await env.AI_HUB.put(IDX_JOBS, JSON.stringify(ids)); } async function getJob(env, id) { const raw = await env.AI_HUB.get(`job:${id}`); if (!raw) return null; try { return JSON.parse(raw); } catch (_) { return null; } } async function putJob(env, job) { await env.AI_HUB.put(`job:${job.id}`, JSON.stringify(job)); } /** comments: — chronological array; newest last. */ async function getComments(env, jobId) { const raw = await env.AI_HUB.get(`comments:${jobId}`); if (!raw) return []; try { const arr = JSON.parse(raw); return Array.isArray(arr) ? arr : []; } catch (_) { return []; } } async function putComments(env, jobId, comments) { await env.AI_HUB.put(`comments:${jobId}`, JSON.stringify(comments)); } function commentsPreview(comments, n) { const limit = n == null ? 3 : n; if (!comments.length) return []; return comments.slice(-limit); } /** Compact status header for dashboard + GET /jobs/:id (Team Norms / Astra). */ function truncateText(s, n) { const t = String(s == null ? '' : s).replace(/\s+/g, ' ').trim(); if (!t) return null; if (t.length <= n) return t; return t.slice(0, Math.max(0, n - 1)).trimEnd() + '…'; } function extractNextOwner(comments) { if (!Array.isArray(comments) || !comments.length) return null; // Scan newest-first for NEXT: for (let i = comments.length - 1; i >= 0; i--) { const body = String((comments[i] && comments[i].body) || ''); const m = body.match(/NEXT:\s*([a-zA-Z][\w-]*)/i); if (m) return m[1].toLowerCase(); } return null; } function extractEvidenceTag(job) { const result = job && job.result; if (result && Array.isArray(result.evidence) && result.evidence.length) { const tags = result.evidence.map((e) => e && e.tag).filter(Boolean); // Prefer strongest runtime evidence if present const order = ['device-tested', 'tests-passed', 'source-inspected', 'probe-only', 'agent-reported']; for (const t of order) { if (tags.includes(t)) return t; } return String(tags[0]); } if (result && result.evidence_tag) return String(result.evidence_tag); if (job && job.decision) return 'agent-reported'; return null; } function extractRevision(job) { const result = job && job.result; const summary = result && typeof result.summary === 'string' ? result.summary : ''; const m = summary.match(/\bv(\d+(?:\.\d+)*)\b/i) || summary.match(/\brev(?:ision)?\s*[#:.]?\s*([A-Za-z0-9._-]+)/i); if (m) return m[0].startsWith('v') || m[0].startsWith('V') ? m[0] : ('rev ' + m[1]); if (job && job.updated) return 'updated ' + String(job.updated).slice(0, 16).replace('T', ' ') + 'Z'; return null; } function deriveStatusBlock(job, comments) { const stored = (job && job.status_block && typeof job.status_block === 'object') ? job.status_block : {}; const status = String((job && job.status) || ''); let blocker = stored.blocker != null ? stored.blocker : null; if (blocker == null && status === 'changes_requested') { blocker = truncateText(job.decision_note || 'changes requested — see decision note', 160); } if (blocker == null && status === 'assigned' && !(job && job.result)) { blocker = null; } let next_owner = stored.next_owner != null ? stored.next_owner : null; if (next_owner == null) { next_owner = extractNextOwner(comments); } if (next_owner == null) { if (status === 'changes_requested') next_owner = job.owner || null; else if (status === 'done' || status === 'accepted') next_owner = null; else if (status === 'assigned' || status === 'in_progress' || status === 'open') next_owner = job.owner || null; } let acceptance = stored.acceptance_checklist != null ? stored.acceptance_checklist : null; if (acceptance == null && job && job.done_when != null) { acceptance = truncateText(job.done_when, 220); } return { revision: stored.revision != null ? stored.revision : extractRevision(job), acceptance_checklist: acceptance, evidence_tag: stored.evidence_tag != null ? stored.evidence_tag : extractEvidenceTag(job), blocker: blocker, next_owner: next_owner }; } async function enrichJob(env, job) { const comments = await getComments(env, job.id); return { ...job, status_block: deriveStatusBlock(job, comments), comments_preview: commentsPreview(comments, 3), comments_count: comments.length }; } function normalizeJobInput(body, existing) { const base = existing || {}; const ts = nowIso(); const id = base.id || body.id || newId(); const status = body.status != null ? String(body.status) : (base.status || 'open'); if (!JOB_STATUSES.has(status)) { return { error: `invalid_status: ${status}` }; } return { job: { id, title: body.title != null ? String(body.title) : (base.title || ''), owner: body.owner != null ? String(body.owner) : (base.owner || null), reviewer: body.reviewer != null ? String(body.reviewer) : (base.reviewer || null), status, project: body.project != null ? String(body.project) : (base.project || null), inputs: body.inputs !== undefined ? body.inputs : (base.inputs ?? null), done_when: body.done_when != null ? String(body.done_when) : (base.done_when || null), result_path: body.result_path !== undefined ? body.result_path : (base.result_path ?? null), result: body.result !== undefined ? body.result : (base.result ?? null), decision: body.decision !== undefined ? body.decision : (base.decision ?? null), decision_note: body.decision_note !== undefined ? body.decision_note : (base.decision_note ?? null), status_block: body.status_block !== undefined ? body.status_block : (base.status_block ?? null), created: base.created || ts, updated: ts } }; } async function readJson(request) { try { return { body: await request.json() }; } catch (_) { return { error: 'bad_json' }; } } function matchJobPath(pathname) { // /jobs/:id or /jobs/:id/complete|decision|comments const m = pathname.match(/^\/jobs\/([^/]+)(?:\/(complete|decision|comments))?$/); if (!m) return null; return { id: decodeURIComponent(m[1]), action: m[2] || null }; } export default { async fetch(request, env) { const url = new URL(request.url); const cors = corsHeaders(); const path = url.pathname.replace(/\/+$/, '') || '/'; if (request.method === 'OPTIONS') { return new Response(null, { headers: cors }); } // PWA dashboard + assets if ((path === '/' || path === '/app') && request.method === 'GET') { return new Response(dashboardHtml(), { headers: { 'Content-Type': 'text/html; charset=utf-8', ...cors } }); } if (path === '/manifest.webmanifest' && request.method === 'GET') { return new Response(JSON.stringify(manifestJson(url.origin)), { headers: { 'Content-Type': 'application/manifest+json', ...cors } }); } if (path === '/icon.svg' && request.method === 'GET') { return new Response(iconSvg(), { headers: { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400', ...cors } }); } // GET /health if (path === '/health' && request.method === 'GET') { return json({ ok: true, service: 'ai-hub', ts: Date.now() }); } // --- Additive read-only MCP + SRP JSON (do not replace other routes) --- const mcpDeps = { origin: url.origin, corsHeaders, getJobIds, getJob, getComments, enrichJob, deriveStatusBlock, commentsPreview, srpGovernanceMd, srpStandingDelegatedGovernanceMd }; if (path === '/mcp') { return handleMcp(request, env, mcpDeps); } if (path === '/srp/state' && request.method === 'GET') { const state = await buildSrpState(env, mcpDeps); return json(state); } if (path === '/srp/governance' && request.method === 'GET') { return json(buildSrpGovernance(mcpDeps)); } if (path === '/srp/evidence' && request.method === 'GET') { return json(buildSrpEvidence(mcpDeps)); } // --- Additive research orchestration (research:/orch: KV only) --- const orchDeps = { requireAuth, json, readJson, nowIso, newId, corsHeaders, putInbox: async (e, agent, message) => { const rawInbox = await e.AI_HUB.get(`inbox:${agent}`); let inbox = []; try { inbox = rawInbox ? JSON.parse(rawInbox) : []; } catch (_) { inbox = []; } if (!Array.isArray(inbox)) inbox = []; inbox.push(message); if (inbox.length > 50) inbox = inbox.slice(-50); await e.AI_HUB.put(`inbox:${agent}`, JSON.stringify(inbox)); } }; { const orch = await handleResearchRoutes(request, env, orchDeps); if (orch) return orch; } // Public FIELD / Phase A docs (no auth — readable by other agents) if (request.method === 'GET' && (path === '/docs' || path === '/docs/')) { return new Response(docsIndexHtml(url.origin), { headers: { 'Content-Type': 'text/html; charset=utf-8', ...cors } }); } if (request.method === 'GET' && (path === '/mock' || path === '/mock/')) { return textDocResponse(mockHtml, 'text/html; charset=utf-8', cors); } const docsMatch = path.match(/^\/docs\/([^/]+)$/); if (request.method === 'GET' && docsMatch) { const key = docsMatch[1]; const asset = PUBLIC_ASSETS[key]; if (asset) { return new Response(asset.body, { headers: { 'Content-Type': asset.type, 'Cache-Control': 'public, max-age=300', ...cors } }); } const doc = PUBLIC_DOCS[key]; if (!doc) return json({ error: 'not_found', doc: key }, 404); return textDocResponse(doc.body, doc.type, cors); } // GET /agents (heartbeat keys only) if (path === '/agents' && request.method === 'GET') { const list = await env.AI_HUB.list({ prefix: 'agent:' }); const agents = []; for (const key of list.keys || []) { if (!key.name.endsWith(':heartbeat')) continue; const name = key.name.slice('agent:'.length, -':heartbeat'.length); const raw = await env.AI_HUB.get(key.name); let hb = null; try { hb = raw ? JSON.parse(raw) : null; } catch (_) { hb = { raw }; } agents.push({ name, heartbeat: hb, key: key.name }); } return json({ agents, count: agents.length }); } // GET /james — broadcast UI if (path === '/james' && request.method === 'GET') { return new Response(jamesHtml(), { headers: { 'Content-Type': 'text/html; charset=utf-8', ...cors } }); } // GET /james/messages if (path === '/james/messages' && request.method === 'GET') { const raw = await env.AI_HUB.get('james:messages'); let messages = []; try { messages = raw ? JSON.parse(raw) : []; } catch (_) { messages = []; } if (!Array.isArray(messages)) messages = []; return json({ messages, count: messages.length }); } // GET /inbox/:agent const inboxMatch = path.match(/^\/inbox\/([^/]+)$/); if (inboxMatch && request.method === 'GET') { const agent = decodeURIComponent(inboxMatch[1]).trim(); const raw = await env.AI_HUB.get(`inbox:${agent}`); let messages = []; try { messages = raw ? JSON.parse(raw) : []; } catch (_) { messages = []; } if (!Array.isArray(messages)) messages = []; return json({ agent, messages, count: messages.length }); } // POST /james — James broadcast if (path === '/james' && request.method === 'POST') { const denied = requireAuth(request, env); if (denied) return denied; const { body, error } = await readJson(request); if (error) return json({ error }, 400); const textBody = body && body.body != null ? String(body.body).trim() : body && body.message != null ? String(body.message).trim() : ''; if (!textBody) return json({ error: 'body_required' }, 400); let to = body && body.to != null ? String(body.to).trim().toLowerCase() : 'all'; const allowed = new Set(['all', 'claude', 'chatgpt', 'grok']); if (!allowed.has(to)) return json({ error: 'to_must_be_all_claude_chatgpt_or_grok' }, 400); const message = { id: newId(), from: 'james', to, body: textBody, at: nowIso(), ts: Date.now() }; const rawLog = await env.AI_HUB.get('james:messages'); let log = []; try { log = rawLog ? JSON.parse(rawLog) : []; } catch (_) { log = []; } if (!Array.isArray(log)) log = []; log.push(message); if (log.length > 100) log = log.slice(-100); await env.AI_HUB.put('james:messages', JSON.stringify(log)); const note = to === 'all' ? textBody : `@${to}: ${textBody}`; const heartbeat = { agent: 'james', note, at: nowIso(), ts: Date.now(), broadcast_id: message.id, to }; await env.AI_HUB.put('agent:james:heartbeat', JSON.stringify(heartbeat)); const targets = to === 'all' ? ['claude', 'chatgpt', 'grok'] : [to]; for (const agent of targets) { const rawInbox = await env.AI_HUB.get(`inbox:${agent}`); let inbox = []; try { inbox = rawInbox ? JSON.parse(rawInbox) : []; } catch (_) { inbox = []; } if (!Array.isArray(inbox)) inbox = []; inbox.push(message); if (inbox.length > 50) inbox = inbox.slice(-50); await env.AI_HUB.put(`inbox:${agent}`, JSON.stringify(inbox)); } return json({ ok: true, message, heartbeat, delivered_to: targets }, 201); } // POST /reply — agent → James (or another agent) if (path === '/reply' && request.method === 'POST') { const denied = requireAuth(request, env); if (denied) return denied; const { body, error } = await readJson(request); if (error) return json({ error }, 400); const from = body && body.from != null ? String(body.from).trim().toLowerCase() : ''; const agentSenders = new Set(['claude', 'chatgpt', 'grok']); if (!from || !agentSenders.has(from)) { return json({ error: 'from_must_be_claude_chatgpt_or_grok' }, 400); } const textBody = body && body.body != null ? String(body.body).trim() : body && body.message != null ? String(body.message).trim() : ''; if (!textBody) return json({ error: 'body_required' }, 400); let to = body && body.to != null ? String(body.to).trim().toLowerCase() : 'james'; const allowedTo = new Set(['james', 'all', 'claude', 'chatgpt', 'grok']); if (!allowedTo.has(to)) { return json({ error: 'to_must_be_james_all_claude_chatgpt_or_grok' }, 400); } const message = { id: newId(), from, to, body: textBody, at: nowIso(), ts: Date.now() }; const rawLog = await env.AI_HUB.get('james:messages'); let log = []; try { log = rawLog ? JSON.parse(rawLog) : []; } catch (_) { log = []; } if (!Array.isArray(log)) log = []; log.push(message); if (log.length > 100) log = log.slice(-100); await env.AI_HUB.put('james:messages', JSON.stringify(log)); // Fan-out inboxes (agents can talk to each other too) const delivered_to = []; if (to === 'james' || to === 'all') { delivered_to.push('james'); } if (to === 'all') { for (const a of ['claude', 'chatgpt', 'grok']) { if (a !== from && !delivered_to.includes(a)) delivered_to.push(a); } } else if (to !== 'james') { // specific agent if (!delivered_to.includes(to)) delivered_to.push(to); } for (const recipient of delivered_to) { const rawInbox = await env.AI_HUB.get(`inbox:${recipient}`); let inbox = []; try { inbox = rawInbox ? JSON.parse(rawInbox) : []; } catch (_) { inbox = []; } if (!Array.isArray(inbox)) inbox = []; inbox.push(message); if (inbox.length > 50) inbox = inbox.slice(-50); await env.AI_HUB.put(`inbox:${recipient}`, JSON.stringify(inbox)); } // Optional: update sender heartbeat with a short replied snippet const snippet = textBody.length > 80 ? textBody.slice(0, 77) + '…' : textBody; const heartbeat = { agent: from, note: `replied to @${to}: ${snippet}`, at: nowIso(), ts: Date.now(), reply_id: message.id, to }; await env.AI_HUB.put(`agent:${from}:heartbeat`, JSON.stringify(heartbeat)); // Material filter for James only — never drop agent-to-agent relays let filtered = false; let filter_reason = null; if (to === 'james') { const parsed = parseStructuredHandoff(textBody, body && body.meta); if (!isMaterialForJames(parsed.meta, parsed.body || textBody)) { filtered = true; filter_reason = 'acknowledgement_only'; } } if (filtered) { return json( { ok: true, message, delivered_to, filtered: true, reason: filter_reason }, 201 ); } return json({ ok: true, message, delivered_to }, 201); } // POST /heartbeat if (path === '/heartbeat' && request.method === 'POST') { const denied = requireAuth(request, env); if (denied) return denied; const { body, error } = await readJson(request); if (error) return json({ error }, 400); const agent = body && body.agent != null ? String(body.agent).trim() : ''; if (!agent) return json({ error: 'agent_required' }, 400); const note = body.note != null ? String(body.note) : null; const heartbeat = { agent, note, at: nowIso(), ts: Date.now() }; await env.AI_HUB.put(`agent:${agent}:heartbeat`, JSON.stringify(heartbeat)); return json({ ok: true, heartbeat }); } // GET /jobs and POST /jobs if (path === '/jobs') { if (request.method === 'GET') { const statusFilter = url.searchParams.get('status'); const ids = await getJobIds(env); const jobs = []; for (const id of ids) { const job = await getJob(env, id); if (!job) continue; if (statusFilter && job.status !== statusFilter) continue; jobs.push(await enrichJob(env, job)); } // newest first by updated jobs.sort((a, b) => String(b.updated || '').localeCompare(String(a.updated || ''))); return json({ jobs, count: jobs.length }); } if (request.method === 'POST') { const denied = requireAuth(request, env); if (denied) return denied; const { body, error } = await readJson(request); if (error) return json({ error }, 400); if (!body || !body.title) return json({ error: 'title_required' }, 400); const norm = normalizeJobInput(body, null); if (norm.error) return json({ error: norm.error }, 400); const job = norm.job; const ids = await getJobIds(env); if (!ids.includes(job.id)) { ids.unshift(job.id); await putJobIds(env, ids); } await putJob(env, job); return json({ ok: true, job }, 201); } } // /jobs/:id[/complete|/decision] const jobMatch = matchJobPath(path); if (jobMatch) { const { id, action } = jobMatch; if (!action && request.method === 'GET') { const job = await getJob(env, id); if (!job) return json({ error: 'not_found' }, 404); const comments = await getComments(env, id); return json({ job: { ...job, status_block: deriveStatusBlock(job, comments), comments_preview: commentsPreview(comments, 3), comments_count: comments.length } }); } if (!action && request.method === 'PATCH') { const denied = requireAuth(request, env); if (denied) return denied; const existing = await getJob(env, id); if (!existing) return json({ error: 'not_found' }, 404); const { body, error } = await readJson(request); if (error) return json({ error }, 400); const norm = normalizeJobInput({ ...body, id }, existing); if (norm.error) return json({ error: norm.error }, 400); // Preserve id norm.job.id = existing.id; norm.job.created = existing.created; await putJob(env, norm.job); return json({ ok: true, job: norm.job }); } if (action === 'complete' && request.method === 'POST') { const denied = requireAuth(request, env); if (denied) return denied; const existing = await getJob(env, id); if (!existing) return json({ error: 'not_found' }, 404); const { body, error } = await readJson(request); if (error) return json({ error }, 400); const job = { ...existing, status: 'done', result: body && body.result !== undefined ? body.result : existing.result, result_path: body && body.result_path !== undefined ? body.result_path : existing.result_path, updated: nowIso() }; await putJob(env, job); return json({ ok: true, job }); } if (action === 'decision' && request.method === 'POST') { const denied = requireAuth(request, env); if (denied) return denied; const existing = await getJob(env, id); if (!existing) return json({ error: 'not_found' }, 404); const { body, error } = await readJson(request); if (error) return json({ error }, 400); const decision = body && body.decision != null ? String(body.decision) : ''; if (!DECISIONS.has(decision)) { return json({ error: 'decision_must_be_accepted_or_changes_requested' }, 400); } const job = { ...existing, decision, decision_note: body.decision_note != null ? String(body.decision_note) : body.note != null ? String(body.note) : existing.decision_note, status: decision, updated: nowIso() }; await putJob(env, job); return json({ ok: true, job }); } // GET/POST /jobs/:id/comments if (action === 'comments') { const existing = await getJob(env, id); if (!existing) return json({ error: 'not_found' }, 404); if (request.method === 'GET') { const comments = await getComments(env, id); return json({ job_id: id, comments, count: comments.length }); } if (request.method === 'POST') { const denied = requireAuth(request, env); if (denied) return denied; const { body, error } = await readJson(request); if (error) return json({ error }, 400); const agent = body && body.agent != null ? String(body.agent).trim() : ''; const text = body && body.body != null ? String(body.body).trim() : ''; if (!agent) return json({ error: 'agent_required' }, 400); if (!text) return json({ error: 'body_required' }, 400); const comment = { id: newId(), agent, body: text, at: nowIso() }; const comments = await getComments(env, id); comments.push(comment); // newest last await putComments(env, id, comments); return json({ ok: true, comment, comments, count: comments.length }, 201); } } } return new Response( [ 'AI Hub Coordination Worker', '', 'GET / /app (dashboard)', 'GET /manifest.webmanifest', 'GET /icon.svg', 'GET /health', 'GET /docs /docs/:slug /mock/', 'GET /jobs?status=', 'POST /jobs (auth)', 'GET /jobs/:id', 'PATCH /jobs/:id (auth)', 'POST /jobs/:id/complete (auth)', 'POST /jobs/:id/decision (auth)', 'GET /jobs/:id/comments', 'POST /jobs/:id/comments (auth)', 'POST /heartbeat (auth)', 'GET /agents', 'GET /james', 'POST /james (auth)', 'POST /reply (auth)', 'GET /james/messages', 'GET /inbox/:agent', 'GET /inbox/james', 'POST /mcp GET /mcp (read-only MCP)', 'GET /srp/state', 'GET /srp/governance', 'GET /docs/hub-mcp-readonly', 'GET /docs/hub-research-orchestration', 'GET /research/schema /research/queue /research/queue/highest', 'GET /research/jobs/:id /research/jobs/:id/transitions', 'POST /research/jobs (auth)', 'POST /research/jobs/:id/transition (auth)', 'POST /research/jobs/:id/handoff (auth)', 'GET /research/brief POST /research/brief/generate (auth)', 'GET/POST /research/claims GET /research/decisions', 'POST /research/seed (auth)', '' ].join('\n'), { headers: { 'Content-Type': 'text/plain', ...cors } } ); } };